The ORM debate in 2026—where I've landed after using them for 10 years
I've flip-flopped on ORMs multiple times over my career. Started thinking they were essential, moved to thinking they were harmful, and now I'm somewhere in the middle. Here's my current position after a decade of production experience.
Where ORMs genuinely help:
CRUD operations are tedious without them. Writing INSERT INTO users (name, email, ...) VALUES ($1, $2, ...) RETURNING id for every entity gets old. ORMs make basic create/read/update/delete operations fast to write and easy to maintain.
Schema migrations and versioning. ORM migration tools (Rails migrations, Alembic, Django migrations) are genuinely excellent. Version-controlled, reversible, sequential schema changes are harder to manage with raw SQL files.
Type safety and IDE support. With TypeScript and modern ORMs like Prisma or Drizzle, you get autocomplete, type checking, and compile-time errors for invalid queries. This catches bugs before runtime.
Basic relationships. user.posts is easier to write and read than a JOIN query for simple cases. When the relationship is straightforward, the ORM abstraction helps.
Connection management. Most ORMs handle pooling, retries, and connection lifecycle. You can do this manually, but it's nice to have it handled.
Where ORMs cause problems:
Complex queries become unreadable. The ORM equivalent of a moderately complex query—a few joins, some conditions, maybe a subquery—is often harder to read than the SQL. You end up debugging the ORM's output rather than just writing the query.
N+1 queries. The classic ORM footgun. users.each { |u| u.posts } generates one query per user instead of a single JOIN. ORMs have solutions (eager loading, includes) but you have to remember to use them.
Performance tuning is harder. When a query is slow, you need to understand what SQL the ORM generates. Then you need to figure out how to convince the ORM to generate different SQL. Sometimes this is straightforward; sometimes it's a multi-hour adventure.
Abstraction leaks. Every ORM has cases where the abstraction doesn't quite fit and you need to understand the underlying SQL anyway. You can't avoid learning SQL; you just add ORM knowledge on top.
Schema constraints are awkward. Partial indexes, complex check constraints, database-level validations—many ORMs handle these poorly or not at all. You end up writing raw migrations anyway.
My current approach:
Use the ORM for simple stuff. CRUD operations, simple relationships, basic queries. Let it handle the boilerplate.
Drop to raw SQL for complex queries. Any query with multiple joins, subqueries, window functions, or complex conditions gets written as SQL. Most ORMs let you execute raw queries and map results back to objects.
Keep queries in the application. Some people advocate for stored procedures or views for complex logic. I prefer keeping queries in application code where they're version-controlled with the code that uses them.
Profile regularly. Use query logging to see what the ORM actually generates. N+1 problems and inefficient queries are obvious when you can see the SQL.
Which ORMs I prefer:
For Python: SQLAlchemy Core (not ORM mode) with type hints, or just raw SQL with psycopg. Django ORM is fine for Django projects.
For TypeScript/Node: Prisma for simple projects, Drizzle for more control, raw queries with proper typing for complex cases.
For Ruby: ActiveRecord is so deeply integrated with Rails that fighting it is pointless. Learn its quirks and work with it.
For Go: Generally avoid ORMs. sqlx with raw queries and struct mapping is the Go way.
The take I'll defend:
You should be comfortable writing raw SQL regardless of whether you use an ORM. SQL is a fundamental skill, not legacy technology. ORMs are productivity tools, not SQL replacements.
What's your ORM philosophy? Anyone found the perfect balance?
0 Comments