Published: April 8, 2023
3
12
120

A lot of requests we receive about @DrizzleOrm are related to the "convenience" APIs - stuff that is present in a lot of popular ORMs and that, for a lot of people, differentiates "ORMs" from "query builders". Let me share our vision of it and a bit of Drizzle history.

When I joined the Drizzle ORM development (yes, I wasn't there from the start, @_alexblokh and @andrii_sherman were building it on their own for about 9 months), its API looked a lot different from the current version.

Among other things, it had some conventional APIs like "findOne" or relational joins, and, believe it or not, it was one of the pain points for the Drizzle ORM users (yep, we had people using Drizzle even 1.5 years ago).

First of all, devs were often confused by the behavior of those APIs. Does it return null or undefined if there is nothing to return? Or does it throw an error? Does it add "limit 1" to the queries automatically? What if I pass "limit 2" instead?

What if there are multiple rows returned from the DB for this query? How does it pick which one to return? Admittedly, some of those questions might've been answered if we had good documentation describing the exact behavior. But...

Even if we had that documentation, not everyone from the team might've read it. One dev reads it, uses the "findOne" correctly and moves on. Later, another dev looks at the code and has no idea how the "findOne" method works, so they have to open the docs and read them all over.

Or, more realistically, they assume its behavior based on how they THINK it should work. Who has time to read the docs anyways? So they start using "findOne" in their code, but they have no idea how it actually works.

This results in a situation when people are using the API and have no idea how it works. So when it behaves not as they expect, they have no idea why, because they had no idea how was it supposed to work in the first place.

So they go and ask us, the ORM developers, "why do I have the 'cannot read property id of undefined' error here? Shouldn't findOne throw an error if it doesn't find anything?", to which we reply "what gave you that idea?".

Almost always the answer is "this is how X works in TypeORM/Prisma", which is a fair point. But who decided that the APIs they invented should work the same in other libraries?

And I know what you're probably thinking. "Just name the methods verbosely, like findOneOrThrow, findOneOrNull, findOneOrUndefined, findOneAndSortByDate..." (We love you @kysely_). But this approach has a downside.

What if a dev needs the behavior that doesn't have a method for it? Where do we stop? Do we keep adding support for all the use cases that our users come up with? This is next to impossible.

Or do we tell them "sorry, your use case is too advanced, go back to raw SQL"? What's the point of the ORM then? I can't describe how frustrating it is when you're using a library and enjoying it, when suddenly some small edge case forces you to rewrite a large piece of code...

...without the library, because it doesn't support it and offers no good fallback. That's why we decided that we're gonna make an ORM that will satisfy ALL users, no matter what functionality they need. But this decision came with its own specifics.

The solution was surprisingly trivial - make an ORM that doesn't abstract you from SQL, but embraces it instead. No matter how many layers of abstraction and convenience APIs the ORM provides, it always boils down to SQL.

So why not utilize the expressiveness of SQL, while providing the best possible TypeScript DX on top of it? You can write literally anything with SQL, there is zero abstraction between your code and the actual queries you run on the DB, and you still feel like you're writing TS.

During the first ORM iterations, the philosophy was "satisfy 95% of the use cases and give raw SQL fallbacks for the rest". Now it is - you guessed it - "If you know SQL, you know Drizzle ORM".

When we came up with this slogan, we thought it will be perfect for everyone. The ORM will be as customizable as the SQL itself, and you can build any logic you need on top of it.

Want to query 1-to-many relation? Do a join and aggregate the results however you want them. Want to get a first row from the query? Add a ".limit(1)" (or don't!) and take the first element from the array (or throw an error if there are none, or return null, or undefined...)

You got the idea. It was so perfect on paper. But then it happened. We started to gain popularity and receive feedback from a lot of people. And don't get me wrong - some people absolutely loved the approach, which made us very happy and boosted our confidence to the outer space.

But there was other kind of feedback. And then it hit us. Not all people know SQL! And not everyone wants to write a .reduce() every time they need to query a user with all its posts! So what can we do to satisfy those users?

It may look like we've made a full circle and are now at the step one, but there is a big difference. This time, we had a solid foundantion for the ORM that we are confident in and that covers any potential use case you may have - albeit with some effort on the user side.

Now anything can be built on top of this foundation - be it an alternative opinionated query builder, integrations with other ORMs and query builders, or even community libraries and helpers! It won't change our core and our philosophy, but on the contrary, expand it.

Why do we consider Drizzle an ORM and not a query builder? Not because it should abstract you from SQL, like a lot of people tend to think - but because it provides you a full package for managing your data layer, not just querying your DB.

So, as a conclusion - will Drizzle provide convenience APIs? Yes. Will you be able to build them yourself? Absolutely. Can you expect people to build their own? They are doing it already!

Share this thread

Read on Twitter

View original thread

Navigate thread

1/25