ORMs Are Annoying! Until You Try Living Without One

Gaurang PansareGaurang Pansare
11 min read

When I first got into web development during my college days, the process was refreshingly straightforward. I would set up a PHP backend, execute my SQL queries using mysqli_query, and that was it—no models, no layers, just raw queries and the data I needed.

$conn = mysqli_connect("localhost", "root", "", "mydb");
$result = mysqli_query($conn, "SELECT * FROM users WHERE id = 1");

Then I was introduced to Hibernate ORM in Java. Later, Sequelize ORM in Node.js. And eventually, Mongoose ODM with MongoDB. Suddenly, everything was an object, everything had a schema, and everything required a model definition.

For those unfamiliar, ORM (Object Relational Mapping) and ODM (Object Document Mapping) tools let you interact with your database using objects in your programming language — instead of writing raw SQL or database commands. ORMs are typically used with relational databases like MySQL or PostgreSQL, while ODMs are used with document databases like MongoDB. So instead of crafting a SQL SELECT statement, you call something like User.findAll() or User.save() in your code, and the ORM or ODM library handles the database part under the hood.

My Initial Reaction: “These People Are Mad!!”

I vividly remember my initial reaction while learning about Hibernate ORM during a Java crash course at my first job: “WHHHATT?! These Java people are MAD!” Why does everything need to be a class, a model, or a POJO object? If I can retrieve my data with a one-line SQL query, why must I now split that across three files and define models, relationships, and repositories just to achieve the same result?

It felt like some kind of object-worship cult. SQL — a perfectly capable language — was suddenly not allowed to sit next to Java. No, no. We must now jump through a flaming hoop of entity files and framework magic just to avoid writing SQL directly.

Then Came Sequelize... And Then Mongoose

When I switched to Node.js, we used Sequelize ORM. Same story — more models, more boilerplate, more time spent reading docs just to figure out how to define a basic relationship between entities.

Even after working with Sequelize for years, I would still have to go back to their documentation on associations to understand how to use hasOne, belongsTo, and through. Every. Single. Time.

Then came MongoDB and the Mongoose ODM. And my confusion reached a whole new level. MongoDB is supposed to be schema-less, right? And JavaScript isn’t even a typed language. So why the heck are we defining schemas?

People often say that ORMs make it easy to switch between relational databases — like swapping MySQL for PostgreSQL — because they abstract away the underlying database. Fair enough. But in the case of MongoDB, what exactly am I switching to? Sure, other document databases exist — but Mongoose is tightly coupled to MongoDB, so the whole “you can switch databases” argument kind of falls apart here.

And let’s be real — no one’s casually swapping out their database on a Tuesday afternoon. Even if you did want to, it’s rarely seamless. You can’t just unplug Postgres and plug in MySQL and expect things to work because “we use an ORM”. If someone has actually done that flawlessly, please message me — I need to see this miracle firsthand.

Out of a mix of curiosity and a bit of rebellion, I once wrote a quick script for a one-off task that just updated some data in MongoDB. I didn’t bother using Mongoose or defining any schema. I just used the native MongoDB Node.js client — no models, no schema validation, nothing. And it worked. Which left me thinking: if this gets the job done, why go through all the extra steps with Mongoose and its overhead?

People also say that with an ORM, developers don’t need to learn SQL — they can just stick to the programming language they already know. But here’s the catch: you still have to learn how to use the ORM! And most of them have their own conventions, syntax, and rules. You didn’t really avoid learning something new — you just traded SQL for something else. Sometimes, something even more confusing.

In fact, I’ve lost count of how many times I had a perfectly valid SQL query — maybe something I got from a colleague or wrote for debugging — and then had to spend extra time figuring out how to express that same logic using the ORM’s syntax. Sometimes it's straightforward, but other times it feels like translating from English to Klingon.

We Think It's Just “Mapping”

I came across a line — maybe in a MongoDB blog or a talk (I honestly can’t recall the exact source) — that really struck a chord and got me thinking:

“MongoDB is schema-less. But your data sure follows some schema.”

That line really made me pause. Just because MongoDB doesn’t enforce a schema doesn’t mean your app doesn’t need one. Without some kind of structure, things quickly spiral into chaos — making the code harder to understand, maintain, and trust.

And that’s where tools like ORMs and ODMs come in. The phrase Object Relational Mapping or Object Document Mapping sounds so harmless. Just a mapping. No big deal.

But we often lose context for why that mapping matters.

The Reality Without ORMs

Let’s imagine a production-grade app built entirely without ORMs or ODMs.

Raw SQL Everywhere

Your queries end up scattered throughout the codebase — sometimes written inline alongside business logic, sometimes tucked away in helper functions. If your team is organized, maybe there’s a dedicated file or folder just for SQL strings. But even then, they’re just raw strings — meaning the compiler, linter, or even your IDE’s IntelliSense can’t catch errors, offer suggestions, or help you refactor them safely.

And that’s the real problem: if there’s any mistake in one of those strings — a wrong column name, a missing comma, ORDER BY before WHERE — you don’t know until the query actually runs.

And if the query is buried in a rarely-used API endpoint? That bug could sit unnoticed for months. Then one day, someone calls that API, and boom — SQL error in production.

Now imagine someone renames a column or changes a data type in the DB. Good luck finding every instance of that column across your codebase. Miss one, and you’ll find out the hard way.

Conditional Joins Get Messy

Let’s say you’re building an API to fetch employee details. If the employee is an intern, include the university_address table. If they’re a manager, include the office_address table. Both have similar schemas, but they’re still separate tables — so your joins and logic must adapt accordingly.

You now have two options:

  • Option 1: Write separate SQL queries for each case. This often results in duplicated logic — like repeating the same SELECT, WHERE, ORDER BY clauses or shared joins — across multiple queries.

  • Option 2: Dynamically build the query with string concatenation. That reduces redundancy, but now no one knows what the final query looks like without running the code.

Now imagine handling 6–8 optional joins like this. Yikes!

Transactions and Multi-Step Logic

Now imagine you need a transaction: create an employee, then insert into employee_address, and based on role, insert into either manager, intern, or director.

Without an ORM, you’re manually building a query block like this:

BEGIN;
INSERT INTO employee (name, role) VALUES ('John Doe', 'manager');
INSERT INTO employee_address (employee_id, address) VALUES (LAST_INSERT_ID(), '123 Office St');
INSERT INTO manager (employee_id, department) VALUES (LAST_INSERT_ID(), 'Engineering');
COMMIT;

Now imagine you have to add logic to conditionally insert into intern or director instead of manager, handle rollbacks, and construct this dynamically in code using string concatenation — things get messy fast.

In the previous section, we saw how conditional joins can complicate a single query. Now imagine chaining multiple such conditionally-executed queries into a single transaction. It becomes more difficult to read, understand or even predict what the final query will look like.

Input Validation and SQL Injection Risks

When you build queries by stitching strings together — especially with values coming from API requests — things can get dangerous fast. You have to be extremely cautious about validating and sanitizing every input, or you risk exposing your app to SQL injection attacks. ORMs take care of this for you by using parameter binding and escaping inputs safely by default. That means one less thing to worry about every time you write a query — and a big step toward keeping your APIs secure.

Lessons From Real Projects

Here are two real work experiences that I’ve faced.

Changing Column Names

At work, we sometimes had to rename DB columns. With JPQL, all I had to do was update the field name in the entity class. Done. The app might throw a few syntax errors on startup, but once fixed, everything stays consistent.

With native queries? Total pain. Every single query using that column had to be updated. And that could be in hundreds! Miss one, and it quietly fails later when someone happens to hit that endpoint.

Environment-Specific Schema Hack

Once, our client didn’t want to set up a separate database instance for pre-prod environment. Instead, they asked us to create a different database schema in the same instance.

Now, Spring Boot allows us to configure a custom strategy to set the schema name. But it only applies to the JPQL queries - the ones that are ORM-based. And since most of our queries were native, we had to create a Java file with the schema names, exclude it from Git, and manually place a different version on each environment's server. We also had to update all our queries to use the schema name from the Java file.

It worked. But it was fragile, ugly, and easily avoidable if we had relied more on ORM features.

What I've come to realize

I used to think that ORMs were just bloated frameworks trying to make simple things harder. But over time, I realized they’re actually solving the problems you don’t even know you’ll have — until it’s too late.

🛠 When You Try to Build Your Own Mini ORM

Let’s be honest — if you’re going to pull out query strings, schema names, and column names into separate files, and then write logic to stitch strings together based on conditions... aren’t you basically building your own mini ORM? You’re solving the exact problems that ORMs are designed to handle.

Only now, it’s more error-prone, less generic, and tightly coupled to your project. ORMs already do this — and they do it better, more reliably, and in a way that works across projects and teams.

🚨 The “ORM But Still Raw SQL” Anti-Pattern

I’ve seen codebases where an ORM was technically in place, but nearly all queries were still written natively. In that case, you’re getting the worst of both worlds — extra boilerplate from the ORM, and all the risks of raw SQL.

To be clear, most ORMs do let you run raw queries — and that’s by design. It’s meant as a fallback, for when you have a complex SQL query that the ORM syntax can’t express easily. But that’s a backup plan, not the main plan. If you find yourself reaching for raw queries by default, you’re not really using the ORM — you’re just working around it.

A lot of times, teams stick with raw queries because learning the ORM feels like extra work. Or they were told to use an ORM, so they did — but without really getting why it matters. And in the end, they miss out on everything the ORM was supposed to help with.

If you’re using an ORM, native queries should be your last resort. The more you rely on raw queries, the more you defeat the purpose of using an ORM in the first place.

🧩 Not Perfect, But Worth It

Sure, they aren’t perfect. Sometimes the queries these ORMs generate are messy. Sometimes they feel overkill for basic stuff. Sometimes, they get in the way. But they solve real problems. They handle transactions, conditional logic, rollbacks, query sanitization — all in your app’s native language. You write clean logic. It handles the mess behind the scenes.

🧱 The Real Value

They’re not just “mappers.” They’re your defense against chaos that give you:

  • 🔒 Safer queries

  • 🔧 Easier maintenance

  • 🔄 Smoother schema changes

  • 🔁 Better handling of relationships, conditional logic, and transactions

✨ It’s not about being able to switch databases or not having to learn SQL. It’s about building something that won’t fall apart when things get complicated.

Summary

ORMs and ODMs can feel like unnecessary overhead in small projects or one-off scripts — and in these cases, skipping them is perfectly fine. But as your codebase grows, raw queries introduce real risks: scattered queries, fragile string concatenation, and painful schema changes. ORMs and ODMs bring structure, safety, and consistency that scale with your application.

This article isn’t about always using ORMs — it’s about understanding why they matter.

You can get things done without them — but when things get complex, you’ll be glad you have them.


P.S. This turned out to be a long one — so if you made it all the way to the end, thank you! 🙌 I hope it gave you something to think about. If you have any questions or thoughts, feel free to leave a comment below. And if you found it helpful, a like or share would really mean a lot! 😊

0
Subscribe to my newsletter

Read articles from Gaurang Pansare directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Gaurang Pansare
Gaurang Pansare

I am a passionate backend developer with expertise in Node.js and JavaScript. I have experience in developing scalable and efficient back-end services, APIs, and microservices using Node.js. My work experience includes working with various companies, startups, and individuals in developing their backend architecture and solving complex issues. My ability to learn and adapt to new technologies quickly positions me as an asset to any organization. I am passionate about creating software that solves complex problems and enhances the user experience. If you are seeking a highly skilled and motivated backend developer, I would welcome the opportunity to work with you.