In Plain Words

Your code has a User object holding a list of Address objects. Your database has a user_account table and an address table, linked by a user_id column that points at a user's primary key (a foreign key). Neither side knows the other's shape. Object-relational mapping is the translation layer between them, and the library that does the translating is called an ORM, short for object-relational mapper.

You describe the mapping once: this class is that table, this attribute is that column, this list is the rows in another table that point back here. From then on you ask for objects. The ORM writes the SQL, sends it through the database driver and turns each returned row into an object.

The friction it hides has a name, the object-relational impedance mismatch. Objects nest and reference each other in memory, while a relational database stores flat rows in tables joined by keys. Martin Fowler called the mapping problem hard in his 2012 essay OrmHate and estimated that an ORM handles about 80-90% of it. For the rest you still read and write SQL.

How It Works

Full-featured ORMs such as Hibernate and SQLAlchemy run the same cycle:

  1. Mapping metadata: you declare models. In Django, each model generally maps to a single database table and each attribute represents a field. A relationship, such as a user's list of addresses, maps to a foreign key column.
  2. Query and row mapping: you call a method. The ORM emits SQL and rebuilds objects from the result set, the work Hibernate's docs describe as flattening graphs of objects to tables and rebuilding them from flat SQL query result sets.
  3. Identity map: inside one session (the object that holds loaded objects and the open transaction) the ORM keeps one object per primary key, so loading the same row twice hands back the same object.
  4. Change tracking and flush: the session watches the objects it loaded. Change an attribute and it writes the matching UPDATE when it flushes, before the next query or at commit. SQLAlchemy calls this the unit of work pattern; Hibernate calls it automatic dirty checking.

We ran that cycle on SQLAlchemy 2.1.8. The User and Address models are adapted from its ORM quick start, with 100 users of one address each in in-memory SQLite on Python 3.11.15, and a listener counting every statement sent. The snippets are simplified from the run script for reading:

# 1. lazy loading, the default for relationship()
for user in session.scalars(select(User)):
    len(user.addresses)

# 2. eager loading with selectinload
stmt = select(User).options(selectinload(User.addresses))
for user in session.scalars(stmt):
    len(user.addresses)

# 3. change tracking: no save() call anywhere
user = session.get(User, 1)
user.fullname = "First User"
session.commit()
SnippetStatements sentWhat the ORM did
1. Lazy loading101 SELECTsOne query for the users, then SELECT ... FROM address WHERE ? = address.user_id once per user as the loop touched each list
2. selectinload2 SELECTsOne query for the users, one for all addresses with WHERE address.user_id IN (?, ?, ...)
3. Attribute change1 UPDATEUPDATE user_account SET fullname=? WHERE user_account.id = ?, emitted at commit

Snippet 1 is the N+1 problem. SQLAlchemy's loading guide defines it as N loaded objects whose lazy-loaded attributes cause N+1 SELECT statements, and lazy loading is the default for every relationship() that does not say otherwise. Nothing in the loop looks like a query. Snippet 3 is the unit of work: the ORM generated the UPDATE from a plain attribute assignment at commit.

Why It Matters

Rails and Django ship an ORM built in (Active Record and the Django ORM). Hibernate, SQLAlchemy, Entity Framework Core, Prisma and Drizzle fill the same role in Java, Python, .NET and TypeScript, so a model class in any of them is object-relational mapping at work.

The payoff is less hand-written data-access code and queries the compiler can check. Hibernate's introduction states the goal of ORM as eliminating fragile and untypesafe code, and the EF Core docs say it removes most of the data-access code you would otherwise write.

What you give up is distance from the SQL you actually send. Hibernate's maintainers call accidental lazy fetching and the N+1 selects it causes a legit concern they worry about too. Their answer to ORM or plain SQL is use both: JPA and Hibernate were designed to work alongside handwritten SQL. When a generated query is slow, the database only ever sees SQL, so you fix it in SQL terms, with an eager load, a hand-written query, or the planner tools covered in our look at the Postgres 19 query planner.

In TypeScript the mapping layer itself keeps changing. Prisma ORM v7 runs without Rust engines by default and compiles queries to SQL with a TypeScript-based compiler (what that change did to Prisma 7). Drizzle describes itself as a headless TypeScript ORM with both relational and SQL-like query APIs, and it has gone after the per-row cost of turning driver rows into typed objects (our post on the Drizzle ORM tax). Picking between the two is a separate Drizzle vs Prisma decision.

Common Confusions

TermWhat it isHow it differs from an ORM
Query builderA library that builds SQL from typed method calls, such as KyselyReturns rows shaped by the query, with no identity map or change tracking. The line blurs in TypeScript: Prisma calls its client a type-safe query builder inside Prisma ORM.
Raw SQLStatements you write and send through a driverNothing hidden, and you write the row-to-object code yourself. The Hibernate team recommends mixing both.
ODMObject-document mapper, such as Mongoose for MongoDBMaps objects to documents in a document database, not to rows in tables.
Active Record patternAn object that wraps a table row and carries its own data access (Fowler)One way to build an ORM: the model saves itself. Rails' Active Record implements it.
Data Mapper patternA separate layer that moves data between objects and the database (Fowler)The other common design: objects stay unaware of the schema and the mapper does the saving.
JPA (Jakarta Persistence)A Java specification for object-relational persistenceA spec, not a library. Hibernate inspired it and implements it in full.