Skip to main contentLogo

Command Palette

Search for a command to run...

ORM — The Friendship Between Code and Databases

Published on
Apr 14, 2025
ORM — The Friendship Between Code and Databases

Diving into ORM — The Friendship Between Code and Databases (and Sometimes Rivalry :)

Today we dive into one of the most discussed topics in programming—sometimes loved, sometimes making you say “eh, I’d just write SQL”—ORM (Object-Relational Mapping). Every experienced developer crosses paths with ORM. Some befriend it closely; others keep a distance. Let’s see what ORM is, why it exists, and what benefits (or headaches) it brings.

Our goal here is to explain what ORM is—its philosophy, background, and mechanics—in depth, without going too deep into specific specifications like JPA for now. The aim is to fully grasp the concept of ORM. When specific examples are needed, we’ll refer to the Java, Spring/Spring Boot ecosystem.

Background of ORM: The “Impedance Mismatch” Problem

It all started with the famous Object-Relational Impedance Mismatch. Let’s admit it—there are fundamental differences between the paradigms of the object-oriented world (OOP) and the structure of relational databases (RDBMS).

  • Object World: Objects, classes, inheritance, polymorphism, composition, complex relationships... Everything revolves around objects and their behaviors.
  • Relational World: Tables, rows, columns, primary keys, foreign keys... Everything is built around structured data and the relations between them.

Bridging these two different worlds has always been a headache:

  • How do you store a List<Order> from Java in a database? A separate orders table? What about the Category object inside a Product object?
  • How do you convert rows from the DB into Java objects one by one? Pulling data with getXXX() from a ResultSet and then passing into setXXX() on the object each time? 🤔 Yes, that’s how we did it before... and it meant lots of boilerplate code. Writing the same mapping code over and over for every new table or column... Those who remember their early career years may smile a little now.
  • How should a DB VARCHAR map to Java String, and NUMBER to Integer or BigDecimal? These type mismatches required additional logic as well.

This is where ORM comes in—to solve this “impedance mismatch,” building a bridge between the object world and the relational world. The goal is to free developers from boring, repetitive SQL and mapping code, allowing them to focus on business logic.

What is ORM? Technical Behind-the-Scenes

Simply put, ORM automatically maps your program’s objects (e.g., Java classes) to tables in the database, and vice versa. This happens behind the scenes, and in many cases the developer doesn’t have to write SQL directly.

⚙️ Core Components and Concepts of ORM

  1. Mapping Metadata: You must tell ORM how to link your objects to tables. This is usually done via:
    • Annotations: The most popular method in Java. Add annotations like @Entity, @Table, @Id, @Column, @OneToMany on classes and fields to define mappings directly in code. (The JPA specification standardizes these, but different ORM frameworks may have their own specific annotations.)
    • XML Configuration Files: Mapping details are stored in separate XML files. Sometimes preferred to keep mapping out of code, but harder to manage.
  2. Session / EntityManager / Context: ORM tools typically operate through a session or context object (in Java, JPA calls this EntityManager). It manages all operations between the program and the database. It’s a kind of Unit of Work. For example:
    • Persist objects (save/persist).
    • Find objects (find/get).
    • Delete objects (delete/remove).
    • Execute queries.
  3. Identity Map: An important optimization. Within the same session/context, it ensures there is only one object instance per row with the same primary key. If you query the same ID repeatedly, ORM returns the same in-memory instance instead of creating new ones. This improves performance and consistency.
  4. Lazy Loading vs. Eager Loading: When objects are related (e.g., Order refers to Customer), when should related objects be loaded?
    • Eager Loading: Loads related objects immediately when the main object loads. Simple, but can fetch unnecessary data and hurt performance (especially with deep or numerous relationships).
    • Lazy Loading: Related objects are not loaded initially. On first access (e.g., order.getCustomer().getName()), ORM sends an extra query behind the scenes. Faster at the start and memory-friendly, but can easily cause the classic N+1 Select Problem if used carelessly.
  5. Caching: To improve performance, ORMs provide caching mechanisms:
    • First-Level Cache: At the Session/EntityManager scope, enabled by default (related to Identity Map). Within the same session, repeat access to the same object is served from cache without hitting the DB. Cleared when the session closes.
    • Second-Level Cache: More global and can be shared across sessions. Requires additional configuration. Useful for often-read, rarely-changed data.
  6. Query Language: ORMs often provide SQL-like languages that operate on objects and their properties (e.g., HQL in Hibernate, JPQL in JPA). This lets developers focus on the object model instead of SQL syntax. There are also programmatic APIs like Criteria. Of course, most ORMs allow running Native SQL when needed.

How ORM Works: A Real-Case Scenario

Let’s see what happens behind the scenes with a simple example. Suppose we have a Product class and a corresponding products table.

entities/Product.java
// Conceptual Entity Class (with JPA annotations)
@Entity
@Table(name = "products")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) 
    private Long id;

    @Column(name = "product_name", nullable = false) 
    private String name;

    private double price;

    // Getters and Setters...
}

Now let’s look at basic operations (simplified conceptual examples):

Saving an Object

➡️ 1. Saving an Object:

repositories/ProductRepositoryUsage.java
// Assume we use a Spring Data JPA Repository
Product newProduct = new Product();
newProduct.setName("Super Gadget");
newProduct.setPrice(99.99);

productRepository.save(newProduct); 
  • What happens behind the scenes?
    1. productRepository.save(newProduct) is called.

    2. The ORM (e.g., Hibernate) sees this new Product object.

    3. It analyzes the object’s state.

    4. Based on mapping metadata, it generates the appropriate SQL INSERT query:

      sql/insert-products.sql
      INSERT INTO products (product_name, price) VALUES (?, ?)
    5. Binds the parameters ('Super Gadget', 99.99).

    6. Sends the SQL within an active transaction.

    7. If the ID is auto-generated, sets the returned ID back on newProduct.

Fetching an Object

➡️ 2. Fetching an Object:

repositories/ProductRepositoryFind.java
// We want the product with ID = 1
Optional<Product> productOpt = productRepository.findById(1L); 
  • What happens behind the scenes?

    1. productRepository.findById(1L) is called.
    2. The ORM first checks the First-Level Cache (Session/EntityManager) for an object with this ID.
    3. If not in cache: a. It generates the appropriate SQL SELECT query:
    sql/select-product.sql
    SELECT id, product_name, price FROM products WHERE id = ?

    b. Binds the parameter (1L). c. Sends the query to the database. d. Receives the ResultSet. e. Maps data from the ResultSet to a Product object. f. Adds the Product object to the First-Level Cache. 4. If it is in the cache, returns it directly (no DB hit). 5. Returns the result (a Product inside an Optional).

Updating an Object

➡️ 3. Updating an Object:

repositories/ProductRepositoryUpdate.java
// First, find the object
Optional<Product> productOpt = productRepository.findById(1L);
if (productOpt.isPresent()) {
    Product productToUpdate = productOpt.get();
    productToUpdate.setPrice(109.99); // Change the state

    // In Spring Data JPA, save is used for updates as well
    productRepository.save(productToUpdate); 
}
  • What happens behind the scenes (usually at transaction end or on flush):

    1. ORM knows productToUpdate is managed by the Session/Context.
    2. It compares the original and current states (Dirty Checking).
    3. When a change is detected (price changed): a. It generates the appropriate SQL UPDATE:
    sql/update-product.sql
    sql UPDATE products SET price = ? WHERE id = ?

    b. Binds parameters (109.99, 1L). c. Sends the query on transaction commit (or flush).

As you can see, ORM saves us from writing raw SQL and manual mapping. But this convenience comes with a COST...

ORM vs. Specification: An Important Distinction

In technical discussions, confusion sometimes arises: Is ORM a specification? No.

  • Specification: A set of rules or a contract defining how a technology or interface should work. It doesn’t provide a concrete implementation—just the standards.
    • Examples: Java Servlet API, JDBC API, JPA (Java Persistence API). These are specifications. For example, JPA says there should be an @Entity annotation, and the EntityManager interface should provide certain methods, etc.
  • ORM (Object-Relational Mapping): A technique, concept, or design pattern. It’s an approach to solving the problems between objects and relational DBs.
  • ORM Implementation (Framework/Library): Concrete tools that implement the ORM concept. They may implement a specific specification (e.g., JPA).
    • Examples: Hibernate (Java, JPA implementation), EclipseLink (Java, JPA implementation), MyBatis (Java; more of an SQL Mapper, but has ORM features), SQLAlchemy (Python), Entity Framework Core (.NET), Dapper (.NET, micro-ORM).

Why the confusion? In the Java world, Hibernate became so popular and so closely tied to the JPA standard that people sometimes say “JPA” when they mean Hibernate or ORM in general. Technically, however, they’re different: JPA is a specification, and Hibernate (among others) is a framework that implements that specification and applies the ORM technique.

➡️ We’ll delve deeper into the differences between specifications and their implementations in future posts—especially JPA and its implementations.

Pros and Cons of ORM: Finding the Balance

Like any technology, ORM has strengths and weaknesses. It’s not a “silver bullet”—just a tool.

📈 Pros

  • Productivity and Speed: Biggest advantage. Greatly reduces time spent writing SQL and doing mapping for CRUD operations. Developers can focus more on business logic.
  • Database Independence: In theory, ORM makes switching between databases (e.g., PostgreSQL to MySQL) easier by abstracting syntax differences (though not always seamless in practice).
  • Object-Oriented Approach: Your code remains object-oriented. You work with domain objects rather than DB structures.
  • Built-in Functionality: Provides complex features out of the box like caching, lazy loading, transaction management, optimistic locking.
  • Better Maintainability: Data access logic becomes more centralized and standardized. When the table structure changes, updating the mapping (annotations or XML) is usually enough.

📉 Cons

  • Performance Overhead: ORM is an abstraction layer and introduces overhead. Hand-optimized SQL will almost always perform better than ORM-generated queries.
  • Complexity and Learning Curve: ORMs are complex systems. Understanding how they work, how to configure and optimize them (lazy/eager loading, caching, solving N+1) requires time and experience.
  • Leaky Abstraction: Even if ORM tries to hide SQL, to solve performance issues or optimize complex queries, you often need to understand and influence the SQL it generates. The abstraction “leaks.”
  • Complex Queries: Queries with many joins, groupings (GROUP BY), subqueries, or DB-specific features (e.g., window functions) can be hard or inefficient in ORM query languages. In such cases, writing raw SQL is more appropriate.
  • The “N+1 Select Problem”: Classic performance issue arising easily when lazy loading is misconfigured or used carelessly.

ORM — Silver Bullet or Useful Tool?

So, what is ORM? A magic wand? Of course not. It’s a powerful tool created to solve specific problems.

When to Use ORM?

  • If your project is primarily built around CRUD operations.
  • If object-oriented design and your domain model are important (Domain-Driven Design).
  • If increasing development speed is a priority.
  • If your team has ORM experience or is ready to learn.

When to Be Careful or Consider Alternatives?

  • In parts where performance is critical.
  • When writing very complex, DB-specific reports or analytical queries.
  • When performing large-scale data bulk operations (although some ORMs provide optimizations).
  • If you lack resources (time, knowledge) to manage ORM complexity (perhaps a lighter micro-ORM or even plain JDBC/SQL fits better).

Above all, don’t use ORM blindly—understand how it works. Knowing what happens behind the abstraction helps you foresee problems and solve them effectively.


Remember: even the best tool is useless if used incorrectly. ORM is no exception. Used wisely, it can become one of your best allies. Used poorly... you may spend your nights debugging. 😉

Thanks for reading.