Saturday 3 December 2022, 22:30PM
Prisma is a self described "Next-generation Node.js and TypeScript ORM", but if you clicked on this link then you probably already knew that. It differentiates itself from other traditional ORMs like TypeORM or Hibernate via a number of ways, most notably:
node_modules
where you use it as if it was a regular third party library.I can't remember where I first heard about Prisma, but it was around 2 years ago. At the time, despite the fact that I was greatly impressed with what it could do and the approach it was taking, it seemed too new and yet to be considered battle-hardened enough. Various features such as transactions were missing, and the small community/userbase meant naturally meant documentation and bugs were not yet battle-tested. But recently over the past 8 months or so, I've started trying it out in a real, professional and production environment for some new projects I've been working on. So, here goes my review - in the hopes that someone can use this to make their own informed decision in the future.
For starters, Prisma has been by far the most type-safe experience in using databases I've ever had, without a doubt. Trying to access an object's relational field when I forgot to do a join in the query now gives me type errors, which is great. The usual gotchas of not having type safety is pretty much entirely gone, e.g.
Code generation also means a few nice things, like e.g. postgres enums you declare in the schema are generated into actual TypeScript enums as well. There's really not much to say here, other than the fact that Prisma absolutely delivers its promise of maximum type safety for those who seek it. Another benefit I'm seeing personally, is that you can codegen into a custom location inside a monorepo so that multiple applications can take advantage of the same database schema/connection.
ORMs traditionally rely on the database models being defined that ORM's language somewhere. For OOP languages it's often as classes that are heavily annotated with decorators, like TypeORM or MikroORM. I think this has been an okay approach in the past, but there's plenty of pitfalls with this approach:
@Column("varchar")
decorator on a id: number
field.A
as a field in B
and B
as a field in A
but no one tells you if you've done them wrong.The other approach is something similar to Mongoose, which is just declaring your model fields and constraints as an object literal instead. In theory, this should be similarly as type safe as the decorator approach, but in practice most libs which do this approach were largely during the days before type safety was considered as high a priority as it is now.
The main pitfall here is that most general purpose langauages (including TypeScript) just aren't good at describing or enforcing data models and (bi directional) relations. So, the Prisma team said screw it, we're gonna write a custom language for this then generate the TypeScript types and client instead. And in my opinion, that was a fantastic idea. Looking at the example (which can also be found on the main Prisma docs):
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
}
It doesn't look particularly different from classes and decorators at first, but there's some nuance here. This custom DSL can:
number
and "int"
.?
instead of a | null
. No need to deal with undefined
vs null
, because undefined
doesn't exist at the database level.The real kicker here though, is that Prisma has an LSP integration with VSCode (or vim!). This means if you forget to declare relations bidirectionally, or have any ambiguous references then you'll know during development and the codegen step.
Prisma has a super simple PrismaClient
API, which doesn't require much to setup. All that's needed is the Prisma client instance, which gives you access to everything you need. So, bonus points for not having to set up a million Repository
classes and helpers.
This one's pretty simple - Prisma supports not only running your migrations for you, but also generating them from changes in your Prisma schema. This isn't really a groundbreaking feature, but a nice one to have nonetheless. One thing I initially disliked is that it didn't have rollback migrations, which is something that many other database migration systems have. However, upon some thought I realized that this is probably the more correct design choice anyways. After all, once you deploy a migration in production, it's deployed and any "down" migrations aren't guaranteed to actually reverse the effects of the forward migration, e.g. if you've dropped a non-null column in a non-empty table, there's not really any going back. So despite it feeling awkward at first, the lack of down/revert/rollback migrations have made our team think more explicitly about what changes are going to happen.
Far from a dealbreaker, but not ideal. There are a few third party solutions out there that will compile multiple prisma schema files together, but a first party solution doesn't feel like a lot to ask for - especially as the Prisma team is pushing the narrative that it's ready for production, and many databases out there have large numbers of tables which is going to result in a schema file that's thousands of lines long. Given that splitting code into different files/modules isn't exactly groundbreaking computer science these days, I would've really thought this would be done by now.
Nested select queries work like a dream but like many other ORMs, more advanced queries like group-bys with nested relations or complex aggregates aren't really possible, at least not without falling back to raw SQL. Prisma also doesn't have a querybuilder API that can act as an intermediate step between the high level ORM API and raw SQL, which makes sense given their safety-first design philosophy, but also is sometimes missed for more complex queries.
This one's pretty self explanatory. I don't really see any reason (apart from the complexity of the query generation) why we shouldn't be able to do a nested create-many like described here. Similarly, I also think there should be nested programmatic deletes rather than just specifying the cascade behaviour - after all, nested deletes already kind of exist when updating objects without deleting the parents. The API could just allow specifying (for one-to-one relations) which relations should be deleted along with the parent or not.
Ok, so the migrations API is actually not that perfect. Its features I mentioned earlier are still very much great, but I do have trouble with a few things:
prisma migrate dev
command at first thought feels quite useful, but in my opinion is extremely dangerous, because it can reset or wipe your database. The only way to create a migration and not apply it immediately is to use prisma migrate dev --create-only
, is exactly 1 command line argument away from disaster. Combining with the previous point that Prisma recommends running Prisma CLI commands manually against your prod database to resolve failed migrations, it seems incredibly risky to me that someone will eventually press their arrow keys too many times and end up wiping the prod database. Migration creation also happens by looking at your development database and comparing it to your Prisma schema, instead of your previous migrations. This is another design decision which I don't agree with, as local dev database schemas can vary branch to branch and the developer may have forgotten to reset their database when they last switched the branch.Whilst not without its flaws, Prisma has definitely been a big improvement compared to my past experiences in other ORMs. That said, it's not without it's flaws, and whilst I do agree that is is very much a "next generation" ORM, it still has a few of the traditional pitfalls of an ORM - many use cases that don't suit other ORMs will probably not suit Prisma either, at least not yet. However, I'm very optimistic about what's coming in the future, not just with Prisma itself but also future competitors who will have been inspired by Prisma's design approach (particularly, codegen and the conditional generated types) that may come in and drive innovation in this space a bit more.
If you enjoy the above article, please do leave a comment! It lets me know that people out there appreciate my content, and inspires me to write more. Of course, if you really, really enjoy it and want to go the extra mile to support me, then consider sponsoring me on GitHub or buying me a coffee!