← Back to the section

Every Java application that works with a database solves one problem: how to move data from the rows and columns of tables into Java objects — and back again. Let's look at why ORM was invented for this, who JPA and Hibernate are, and when this tool is worth using.

What hurts without ORM: life with plain JDBC

First, let's see what problem ORM actually solves. Imagine a simple task: load an order from the database together with its lines.

String sql = "SELECT o.id, o.status, ol.product_id, ol.quantity " +
             "FROM orders o JOIN order_lines ol ON ol.order_id = o.id " +
             "WHERE o.id = ?";

try (PreparedStatement ps = conn.prepareStatement(sql)) {
    ps.setLong(1, orderId);
    ResultSet rs = ps.executeQuery();

    Order order = null;
    List<OrderLine> lines = new ArrayList<>();
    while (rs.next()) {
        if (order == null) {
            order = new Order(rs.getLong("id"), rs.getString("status"));
        }
        lines.add(new OrderLine(rs.getLong("product_id"), rs.getInt("quantity")));
    }
    if (order != null) order.setLines(lines);
}

The code works, but there is almost nothing about business logic in it — only about shuffling data. Add null handling, several tables, updates — and the volume grows several times over. This is boilerplate: repetitive, mechanical code that is prone to errors when the schema changes.

Object-relational mapping (ORM) is an approach where the framework takes the data shuffling upon itself. You describe how a Java class corresponds to a table, and the framework generates SQL, runs queries, and assembles objects from the results.

A short formula: you work with objects, ORM translates that into SQL.

Who is who: JPA, Hibernate, and Spring Data JPA

Three names that are often confused — let's untangle each one.

JPA (Jakarta Persistence API) is a standard, a specification. A set of interfaces and annotations (@Entity, @Id, @OneToMany, EntityManager) that describe how ORM should work in Java. JPA on its own does no actual work — it is only a contract.

The package in Jakarta EE / Spring Boot 3: jakarta.persistence (before Spring Boot 3 it was javax.persistence).

Hibernate is an implementation of JPA. It contains all the real code: the annotation parser, the SQL generator, connection management, caches. When a Spring Boot application runs a query through JPA, physically it is Hibernate doing the work. Hibernate also offers its own extensions on top of the standard (Session, HQL, specific annotations).

Spring Data JPA is a repository layer on top of JPA/Hibernate. It removes even the minimal code that would remain if you used EntityManager directly: you declare an interface — Spring generates the implementation.

Your code
   ↓
Spring Data JPA (repositories, findBy* methods)
   ↓
JPA / EntityManager (standard API)
   ↓
Hibernate (implementation: SQL generation, caches, session)
   ↓
JDBC
   ↓
Database

For more about Spring Data repositories, see the article Spring Data JPA.

What an Entity is and how it looks

An Entity is a Java class that Hibernate maps to a database table. A minimal example:

@Entity
@Table(name = "orders")
public class Order {

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

    @Column(name = "status")
    private String status;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    private List<OrderLine> lines = new ArrayList<>();

    // constructors, getters, setters
}

Hibernate reads the annotations and knows: Order → table orders, field id → column id with auto-increment, collection lines → related table via @OneToMany.

Now loading the order:

Order order = entityManager.find(Order.class, orderId);

Hibernate generates the SQL itself, runs the query, and assembles the object. The ResultSet mapping has disappeared from your code.

When ORM helps

ORM works well where the application logic revolves around domain objects: creating, modifying, relationships between entities, validations. Typical cases:

  • CRUD operations on individual entities: create an order, change an address, add a line.
  • Navigation through the object graph: order.getCustomer().getAddress() — Hibernate loads the data by itself.
  • Transaction management and dirty checking: Hibernate tracks what changed in an entity and writes the UPDATE at the end of the transaction on its own. You don't need to call save() manually.
  • Caching: the first-level cache (within a single session) is built in by default; the second level is configurable.

When ORM gets in the way

ORM is not a universal tool. There are situations where it adds complexity rather than removing it.

Bulk operations. Updating the status of a million rows through entityManager.find() in a loop is a performance disaster. Hibernate would load each object into memory, track changes, and generate a separate UPDATE. The right path is a JPQL bulk query or native SQL:

// JPQL bulk update — a single SQL query
entityManager.createQuery(
    "UPDATE Order o SET o.status = :status WHERE o.createdAt < :cutoff"
).setParameter("status", "ARCHIVED")
 .setParameter("cutoff", cutoffDate)
 .executeUpdate();

Complex analytical queries and reports. When a query joins 10 tables, uses window functions, GROUPING SETS, complex aggregation — ORM becomes a hindrance. Here native SQL or a specialized tool (jOOQ, for example) is better. ORM is good for domain operations, not for analytics.

Fine-grained control over SQL. If performance is critical and every query is optimized by hand — the ORM abstraction gets in the way. You don't always control what SQL it will generate.

A short formula: ORM is for object-oriented business logic; direct SQL is for bulk operations and analytics.

Persistence Context: Hibernate's central concept

To avoid being surprised by Hibernate's behavior, you need to understand the persistence context. It is the session's working area: a map of all the objects that Hibernate has loaded or created within the current transaction.

When you call entityManager.find(Order.class, 1L):

  1. Hibernate checks: is there an object with id=1 in the persistence context?
  2. If yes — it returns it from memory (without an SQL query).
  3. If no — it runs a SELECT, puts the result into the persistence context, and returns it.

At the end of the transaction, Hibernate walks through all objects in the persistence context, compares them with their original state, and generates an UPDATE wherever something has changed. This is called dirty checking.

@Transactional
public void updateStatus(Long orderId, String newStatus) {
    Order order = entityManager.find(Order.class, orderId);
    order.setStatus(newStatus); // just change the field
    // Hibernate generates the UPDATE at commit — an explicit save() is not needed
}

For more about the persistence context, entity states, and flush, see the article Persistence Context.

In short

  • JDBC requires manual ResultSet → object mapping — lots of boilerplate.
  • ORM takes this mapping upon itself: you describe the class/table correspondence, it generates SQL.
  • JPA is the standard (interfaces and annotations). Hibernate is its implementation. Spring Data JPA is repositories on top.
  • JPA annotations in Spring Boot 3 come from the jakarta.persistence package.
  • ORM is a good fit for CRUD and object-oriented operations; for bulk updates and complex analytics, native SQL is better.
  • The key concept is the persistence context: it tracks loaded objects and writes changes at commit.
  • Entity Mapping — the @Column and @Embedded annotations, type converters, inheritance.
  • Persistence Context — entity states, dirty checking, flush, and detach.
  • Common Hibernate Pitfalls — LazyInitializationException, N+1, and others.
  • Spring Data JPA — repositories, derived queries, @Query on top of Hibernate.