r/programming 7d ago

Solving the 1+N Query Problem

https://acadia.engineering/blog/solving-the-1-plus-N-query-problem
129 Upvotes

94 comments sorted by

View all comments

26

u/JimmyM_1 7d ago

I don't know why people hate on ORMs for this one, yes abstracting SQL can lead some devs to forget / not even know about what is actually happening under the hood, but it isn't that big of a deal!

Make sure your dev team is aware of the JOIN concept and you're good to go!

7

u/Schmittfried 7d ago

To be fair, after using Laravel’s ORM and Hibernate, I’m pretty amazed how far ahead django’s ORM is in terms of usability and making the efficient implementation the path of least resistance. You really have to jump through some hoops to make certain joins efficient with Hibernate that would be trivial with the django ORM. 

3

u/baseballlover723 7d ago edited 7d ago

To be fair, after using Laravel’s ORM and Hibernate, I’m pretty amazed how far ahead django’s ORM is in terms of usability and making the efficient implementation the path of least resistance.

As a ruby person, I never got all the hate that ORMs got, since ActiveRecord was excellent (even if you could still footgun yourself) and I usually felt like I was basically writing sql via ruby DSL most of the time. Never had issues with the simple handful of tables with a few connections between them with it that most uncomplicated DBs look like.

And then I used Hibernate, and I now understand why everyone hates ORMs. I couldn't get forward and backward full object links between relations. I couldn't figure out how to get it to be able to choose if it should load all the sub relations or not. I had N+1 issues because JPA was too dumb to group them all together. I also couldn't get composite primary keys to work with relations either. Just everything felt like it was made hard for no real reason. And the DB isn't even that complicated. It's still a handful of tables with some connections between them.

So now I understand. People don't hate ORMs. They hate shitty ORMs. And there are evidently some popular and shitty ORMs still around for some reason.

3

u/Absolute_Enema 6d ago

 Just everything felt like it was made hard for no real reason

Old Java libs in a nutshell.

1

u/JimmyM_1 7d ago

Haven't worked with either, but that sounds interesting.

I will look them up.

2

u/Schmittfried 7d ago

Honestly I’m still shocked that the Java world just accepts that Hibernate doesn’t offer a way to have safe and still uncomplicated automatic schema migrations without paying for something like Liquibase and using their unreliable Maven plugin. Django has first-class schema migrations built-in, and they just work. Java devs just live with the fact that they have to remember to also write an SQL script for every change they make to an entity class.

2

u/yaboiabrahamlincoln 7d ago

Hibernate does automatic schema migration if you have it turned on, but it can’t do something like detect a column name change and rename the column in the sql. It’ll pretty much treat it as a new column and leave the old one with all the data inaccessible. The way to do it correctly is with an explicit migration script, but it’ll only be relevant for existing dbs that have the old schema, new dbs will start with the new schema. The migration should deal with both cases, or do nothing in case the column is already correct. Not easy to do a column rename conditionally (I don’t know how off the top). So all in all, it’s much easier to do the explicit scripts to avoid this (every db follows the same migrations rather than potentially different paths to the same schema.

I’m curious how django’s orm handles this situation if you have time to respond. Will do some research later because hibernate + flyway has not been super fun for migrations

3

u/Schmittfried 7d ago edited 7d ago

So it doesn’t handle schema migrations. The create.sql feature is nowhere near full migration support in my book (or am I missing something?). 

Django will diff your current models as they are in the code against the projection of applying all past migrations in order. It will then create a new migration file containing the changes necessary to make the virtual DB state match the models. Trivial cases like adding new columns, dropping columns etc. will be generated automatically. For renamed single columns it will ask if those should be a rename or if you removed one column and added another (i.e. drop + add). If multiple fields of the same model were renamed or for any other ambiguities, it will warn you and ask you to write a custom migration. And you can always invoke a command to create a bare migration file for you to fill out with more complex migration logic (custom order of steps, custom SQL, even custom Python).

All of that is DB-agnostic (except for the SQL you write yourself, though you can add multiple versions for different dialects) and defined in pure Python. It never hits a DB until you apply migrations. Migrations are basically an ordered list of Python objects describing changes, with Django providing predefined steps for all common building blocks like adding a column, creating an index etc..

That migration will have an ID and point to the previous ID, which makes them independent of filenames and allows squashing multiple migrations into one when the history becomes expensively long (it will even consolidate counteracting/obsolete changes into no-ops).

It also provides automatic inversion for non-destructive changes and allows you to define custom backwards migrations for destructive changes and custom Python/SQL, so that a well-maintained Django history is always fully reversible and allows jumping to arbitrary past versions.

Last time I checked, native Hibernate doesn’t even cover half of that. People commonly use additional tooling, but Flyway doesn’t support diffing at all and Liquibase needs a (poorly maintained) Maven plugin for that, and even then it will only diff against a reference DB, not a virtual state reconstructed from your migration history. So you have to make sure the DB matches the migration state you’re expecting. Support for custom naming strategies is also quite bad, I still remember I had to fall back to explicit column/index naming a few times because it just wouldn’t apply the naming strategy to the migration file. Support for @Embedded was very lackluster, too. It simply wouldn’t correctly apply column lengths for string columns in embedded types. Not to mention, XML as a migration format sucks and Flyway is just applying arbitrary SQL scripts, so it can’t possibly provide the same level of diffing/analysis/safeguards without solving the halting problem or running all your scripts against a throwaway DB. 

Granted, I think Django doesn’t even support the embedded pattern, but my point is: Django ORM provides one coherent developer experience whereas my options in the Java world feel like I have to glue a set of tools with varying features together and hope my use case is covered by their common denominator. Which is odd, given the whole idea of Spring and similar projects was to provide a coherent and battle-tested experience. 

-1

u/wildjokers 7d ago

ou really have to jump through some hoops to make certain joins efficient with Hibernate

You literally just write a join in HQL. There are no hoops to jump through.

1

u/Schmittfried 5d ago edited 5d ago

If HQL supports all the features you need for your query. Like, any complex query at all. Note the phrasing “certain joins”. Need to build a search query dynamically and still load all the joined tables into your object tree with a single roundtrip? Have fun fiddling around with the criteria API and using the correct join/fetch methods. Need to join multiple many-to-many relationships efficiently (i.e. one top-level query and then one query per relationship instead of one explosive join or N+1 queries)? Enjoy writing all of that logic yourself and fighting the entity cache (with pseudo queries that don’t actually hit the DB and are only there to make Hibernate link the cached entities). Because Hibernate can’t fucking figure it out. With Django it’s always prefetch_related(list of navigational properties) and you’re done. No matter if it’s one relationship or 100, one nesting level or 10, Django picks the right join strategy for you. When I used Hibernate with Spring there were so many situations where I would have preferred to use the underlying EntityManager directly, but that’s not what all the default Spring patterns want you to do, so you choose between fighting Spring or fighting Hibernate.

Also, if you have to resort to HQL you’re basically falling back to SQL in my book. The whole point of ORMs is to not write query boilerplate. 

1

u/wildjokers 5d ago

When I used Hibernate with Spring there were so many situations where I would have preferred to use the underlying EntityManager directly, but that’s not what all the default Spring patterns want you to do, so you choose between fighting Spring or fighting Hibernate.

You literally just inject the EntityManager:

@Service public class AccountService {

private final EntityManager entityManager;

public AccountService(EntityManager entityManager) {
    this.entityManager = entityManager;
}

@Transactional
public void doSomething(Long accountId) {
    Account account = entityManager.find(Account.class, accountId);
    // use entityManager...
}

}

The whole point of ORMs is to not write query boilerplate.

That is not the even remotely the point of ORMs. The point is right in the name Object Relationship Mapper. Its sole purpose to map resultsets to objects.

Need to build a search query dynamically and still load all the joined tables into your object tree with a single roundtrip? Have fun fiddling around with the criteria API and using the correct join/fetch methods. Need to join multiple many-to-many relationships efficiently (i.e. one top-level query and then one query per relationship instead of one explosive join or N+1 queries)?

Just drop down to native SQL. Hibernate will still map your result set to an object.

1

u/Schmittfried 5d ago edited 5d ago

You literally just inject the EntityManager

Sure, now you have to create a custom repo implementation instead of the default way of using repo inferfaces with the method name DSL.

I didn’t say Spring+Hibernate can’t do these things. It’s just that the convenient low-boilerplate parts are extremely limited and doing anything slightly more complex instantly requires you to study the intricacies of the session context and how Spring interferes with that, or create several classes just because you wanted a single join strategy that wasn’t covered by the defaults despite being pretty standard and that would have been a single line with django.

I wish I could give you more specifics, but that experience was 2 years ago and I don’t have access to the code anymore. What burned itself into my memory was how ridiculously troublesome it was to get a query over three 1:n relations including a recursive one (a tree) with a few transitive 1:1 relations working as its supposed to (no combinatorial explosion, no additional N queries when processing the result set and accessing navigational properties).

My entire point is: If it’s this annoying to write efficient queries, many developers will simply not care. They will use their simple generated repo method and be done with it because N+1 queries probably won’t be prohibitively bad in many situations (and I’ve even heard one saying the Hibernate cache figures that out automatically anyway and they won’t try to be smarter than the ORM, vastly overestimating what Hibernate does in this case by default).