After learning JDBC, I was proud of myself. I could connect to a database, run queries and read the results. Then I built a real project with 12 tables, and my code filled up with the same pattern again and again: open a connection, prepare a statement, set five parameters, run it, loop over a ResultSet, copy each column into an object, close everything. Changing one column name meant hunting through dozens of SQL strings.

That's the problem ORM tools were invented to solve, and Hibernate is the most famous one in Java. You describe how your classes map to tables once, and Hibernate writes and runs the SQL for you.

In this post, we'll learn what ORM is and which problem it solves, its pros and cons, what Hibernate is and how it works, how to use it with and without Maven (using PostgreSQL), the common methods you'll use, how Hibernate relates to Jakarta Persistence (JPA), and a step-by-step example in the Spring Framework. I've also added things beginners usually miss: the entity lifecycle, lazy loading, the N+1 problem, caching and versioning.

A version note: I wrote this for Hibernate ORM 7.4 and Spring Framework 7.0. Both need Java 17 or newer. If you work on an older project (Hibernate 5 or 6), the section on older Hibernate methods tells you what changed.

Table of contents

  • What is an ORM?
    • The problem: objects and tables do not match
    • How an ORM solves it
    • The same job, with and without an ORM
    • Popular ORM tools
  • Pros and cons of ORMs
    • Advantages
    • Disadvantages
  • What is Hibernate?
    • How Hibernate fits in
    • The core pieces: SessionFactory, Session and Transaction
    • The life of an entity
  • Setting up PostgreSQL for the examples
  • The common code: entity and main class
    • The entity class
    • The main class
  • Using Hibernate without Maven
    • Step by step without Maven
  • Using Hibernate with Maven
    • Step by step with Maven
  • Common Hibernate methods
    • Saving, reading, updating and deleting
    • Queries with HQL
    • Managing the persistence context
    • Transactions and the safe pattern
    • Methods from older Hibernate versions
  • Mapping relationships and loading data
    • One-to-many and many-to-one
    • Lazy loading and the N+1 problem
  • Hibernate vs Jakarta Persistence
    • What each one is
    • The same code in both styles
    • Which one to code against
  • Hibernate in the Spring Framework
    • Step 1: Add the dependencies
    • Step 2: Create the entity
    • Step 3: Create the repository
    • Step 4: Create the service
    • Step 5: Configure Spring
    • Step 6: Run the application
    • How Spring and Hibernate work together
    • The shortcut: Spring Boot and Spring Data JPA
  • Other things worth knowing
    • Configuration settings
    • Caching
    • Optimistic locking with @Version
    • Managing the schema in real projects
    • Logging the SQL
  • Best practices
  • Which one should I use?
  • Common mistakes
  • Quick recap
  • Try it yourself

What is an ORM?

ORM stands for Object-Relational Mapping. It's a technique (and a kind of tool) that connects the two worlds a Java developer lives in: objects in your code and tables in a relational database. A class maps to a table, an object maps to a row, and a field maps to a column. Instead of writing SQL for every operation, you work with objects, and the ORM does the translating.

The problem: objects and tables do not match

With plain JDBC, you do all the translating by hand. Every query repeats the same steps, and your SQL strings are scattered through the code. That's tiring, but it's not the deepest problem.

The deeper problem is that objects and tables are built on different ideas. This gap has a name: the object-relational impedance mismatch.

 OBJECT WORLD (Java) RELATIONAL WORLD (database) classes, objects, fields tables, rows, columns references between objects foreign keys and joins inheritance (Dog extends Animal) no inheritance (flat tables) identity with == and equals() identity with a primary key walk the graph: s.getCourse().getTitle() write a JOIN in SQL collections: List<Student> a separate table with a foreign key 

Here's what that means in practice:

  • Inheritance: Java has it, tables don't. How do you store Dog and Cat, which both extend Animal?
  • Identity: two Java objects can hold the same data but be different objects. In a database, a row is identified by its primary key.
  • Associations: in Java, a Student holds a reference to a Course. In a database, the student table holds a foreign key column.
  • Navigation: in Java you walk from object to object. In SQL, you join tables.

Writing the code that bridges all this by hand, for every class, is slow and full of bugs.

How an ORM solves it

An ORM does the bridging for you. You describe the mapping once, usually with annotations on your classes. Then the ORM:

  • generates the SQL for creating, reading, updating and deleting objects,
  • turns query results back into objects,
  • handles relationships between objects,
  • tracks which objects changed and saves only those changes,
  • manages transactions and caching,
  • adapts the SQL to each database's dialect.
 Java object table row Student(id=1, name="Asha", marks=88) student: id=1 | name='Asha' | marks=88 | ^ | session.persist(student) | +--- the ORM writes the SQL for you -----------+ insert into student ... 

The same job, with and without an ORM

Saving and loading one student with plain JDBC:

String insert = "INSERT INTO student (name, marks) VALUES (?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(insert)) { ps.setString(1, student.getName()); ps.setInt(2, student.getMarks()); ps.executeUpdate(); } String select = "SELECT id, name, marks FROM student WHERE id = ?"; Student found = null; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(select)) { ps.setLong(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { found = new Student(rs.getLong("id"), rs.getString("name"), rs.getInt("marks")); } } } 

The same two jobs with Hibernate:

session.persist(student); // INSERT Student found = session.find(Student.class, id); // SELECT ... WHERE id = ? 

Two lines instead of twenty, and no column names to copy by hand.

Popular ORM tools

In Java:

  • Hibernate ORM: the most widely used ORM, and the main topic of this post.
  • EclipseLink: another full ORM, and the reference implementation of the JPA standard.
  • Spring Data JPA: not an ORM itself, but a layer on top of JPA (usually Hibernate) that writes repository code for you.

Related tools that are not full ORMs: MyBatis (you write the SQL, it maps results to objects), jOOQ (a type-safe way to write SQL in Java) and Spring's JdbcTemplate (less boilerplate for JDBC).

Other languages have their own: Entity Framework (.NET), SQLAlchemy and the Django ORM (Python), ActiveRecord (Ruby), Prisma and TypeORM (JavaScript and TypeScript).

Pros and cons of ORMs

Advantages

  • Much less boilerplate. No more connection, statement and result set code for every query.
  • You think in objects. Your code stays object-oriented, and SQL is mostly hidden.
  • Relationships are handled for you. You walk from a Student to a Course as if they were ordinary objects.
  • Portability. The ORM generates SQL for the database you use, so switching databases is often a configuration change.
  • Built-in features: change tracking (only modified rows are updated), caching, lazy loading, optimistic locking and schema generation.
  • Safer by default. Values are sent as parameters, which protects you from SQL injection.
  • One place for mapping. Table and column names live in your entity classes, not in dozens of SQL strings.
  • A standard. Code written against JPA works with several ORM providers.

Disadvantages

  • A learning curve. Sessions, entity states, fetching strategies and flushing are real concepts to learn.
  • Hidden SQL. You don't see the queries, so you can create slow ones without noticing. The famous example is the N+1 problem (covered later).
  • Leaky abstraction. You still need to understand SQL, indexes and transactions to tune performance.
  • Not ideal for everything. Bulk updates, complex reports and heavy analytics are often better in plain SQL.
  • Memory and startup cost. The ORM tracks your objects and builds its mapping metadata when the app starts.
  • "Magic" that surprises. For example, changing a field on a managed object can cause an UPDATE without any save call.
  • Overkill for tiny apps. For a few simple queries, JDBC (or JdbcTemplate) may be enough.
  • Schema changes need care. Letting the ORM modify a production schema automatically is risky.

The honest summary: an ORM saves a lot of work, but it doesn't remove the need to understand databases.

What is Hibernate?

Hibernate ORM is an open-source ORM framework for Java. Gavin King started it in 2001, and it became the most popular persistence tool in the Java world. Its ideas strongly influenced the JPA standard (more on that later), and today Hibernate is the most widely used JPA implementation. It also adds many extra features of its own.

Some current facts:

  • Its Maven groupId is org.hibernate.orm, and the main artifact is hibernate-core.
  • Hibernate 7 needs Java 17+ and implements Jakarta Persistence 3.2.
  • At the time of writing, the latest stable series is 7.4 (7.4.13.Final), and Hibernate 8.0 (aimed at Jakarta Persistence 4.0) is still in development.
  • Since version 7.0, it uses the Apache License 2.0.

How Hibernate fits in

Hibernate doesn't replace JDBC. It sits on top of it. Everything you learned about JDBC still happens underneath:

 YOUR APPLICATION (entity classes + Hibernate or JPA API) | v HIBERNATE ORM - reads your mapping (annotations on the entities) - turns your calls and HQL queries into SQL for your database (the "dialect") - tracks changes, caches objects, manages relationships | v JDBC (Connection, PreparedStatement, ResultSet ...) | v JDBC DRIVER (the postgresql jar) --> PostgreSQL 

Every query Hibernate runs becomes a PreparedStatement, so values are always sent safely as parameters. Hibernate also finds out which database you're using from the connection and picks the right dialect on its own, so you usually don't configure one.

The core pieces: SessionFactory, Session and Transaction

Three objects do most of the work:

 SessionFactory one per database, created ONCE when the app starts expensive to create, thread-safe, shared by everyone | | openSession() v Session (also an EntityManager) one per unit of work (for example, one web request) cheap, NOT thread-safe, wraps one JDBC connection holds the persistence context: a cache of the objects it is tracking | | beginTransaction() v Transaction --> commit() or rollback() 
  • SessionFactory: built once from your configuration and mapping. Think of it as the "factory" that makes sessions.
  • Session: your working conversation with the database. You open it, do some work and close it.
  • Transaction: groups the work so it succeeds or fails as a unit (the same idea as in the JDBC post).
  • Persistence context: the "first-level cache" inside every session. Hibernate remembers every object it loaded or saved, and watches it for changes.

The life of an entity

An entity is a Java object that Hibernate manages (one that maps to a table). At any moment, an entity is in one of four states:

 new Student("Asha", 88) | TRANSIENT a plain object; Hibernate doesn't know it; no row exists | | persist() v find() / query results --> PERSISTENT (also called managed) | tracked in the persistence context; changes are saved automatically | | session closes, detach(), | remove() or clear() v | REMOVED (the row is deleted when the work is flushed) v DETACHED has an id and a row exists, but no session is tracking it | | merge() --> returns a PERSISTENT copy 

Three ideas follow from this picture:

  • Dirty checking. For a persistent entity, Hibernate remembers its original values. At commit time, it compares them with the current values and sends an UPDATE only if something changed. You don't call "save" to update a managed object.
  • Flush. The moment Hibernate sends the pending SQL to the database is called a flush. It happens automatically before a query that might be affected and at commit.
  • Identity guarantee. Inside one session, asking for the same row twice gives you the same object.

Setting up PostgreSQL for the examples

All examples use PostgreSQL. The quickest way to get one is Docker:

docker run --name school-db -e POSTGRES_PASSWORD=secret -e POSTGRES_DB=school -p 5432:5432 -d postgres:17 

This starts a PostgreSQL server on port 5432 with a database called school, the user postgres and the password secret. (That password is only for local practice. Use a strong one and read it from an environment variable in real projects.)

If you don't use Docker, install PostgreSQL from postgresql.org, then create the database:

psql -U postgres -c "CREATE DATABASE school;" 

Our connection details for the rest of the post:

  • URL: jdbc:postgresql://localhost:5432/school
  • User: postgres
  • Password: secret

The common code: entity and main class

The Java code is the same whether you use Maven or not. Only the way you get the libraries differs.

The entity class

package com.example; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; @Entity @Table(name = "student") public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, length = 50) private String name; private int marks; protected Student() { // required by Hibernate } public Student(String name, int marks) { this.name = name; this.marks = marks; } public Long getId() { return id; } public String getName() { return name; } public int getMarks() { return marks; } public void setMarks(int marks) { this.marks = marks; } @Override public String toString() { return "Student{id=" + id + ", name='" + name + "', marks=" + marks + "}"; } } 

What each annotation does:

  • @Entity: this class is mapped to a table.
  • @Table(name = "student"): the table name. (Without it, Hibernate uses the class name.)
  • @Id: the primary key field.
  • @GeneratedValue(strategy = IDENTITY): the database generates the id (an auto-increment column). Another common strategy is SEQUENCE, which suits batching better.
  • @Column(nullable = false, length = 50): column settings. A field with no annotation (like marks) is mapped by default, to a column with the same name.

Rules for an entity class:

  • It needs an @Id field.
  • It needs a no-argument constructor (it can be protected, so your own code can't misuse it).
  • It must not be final, because Hibernate creates subclasses (proxies) of entities.
  • Use jakarta.persistence.* imports. The old javax.persistence is from Hibernate 5 and earlier.

With hbm2ddl.auto set to update (we'll set it below), Hibernate creates a table similar to this one:

create table student ( id bigint generated by default as identity, marks integer not null, name varchar(50) not null, primary key (id) ) 

The main class

This program creates a student, reads it, updates it, queries and deletes it, using the classic Session and Transaction style:

package com.example; import java.util.List; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.jpa.HibernatePersistenceConfiguration; public class Main { public static void main(String[] args) { try (SessionFactory sessionFactory = buildSessionFactory()) { Long id = createStudent(sessionFactory, "Asha", 88); System.out.println("Saved with id " + id); System.out.println("Found: " + findStudent(sessionFactory, id)); updateMarks(sessionFactory, id, 95); System.out.println("After update: " + findStudent(sessionFactory, id)); System.out.println("Top students: " + findTopStudents(sessionFactory, 90)); deleteStudent(sessionFactory, id); System.out.println("Deleted student " + id); } } private static SessionFactory buildSessionFactory() { return new HibernatePersistenceConfiguration("school") .managedClass(Student.class) .jdbcUrl("jdbc:postgresql://localhost:5432/school") .jdbcCredentials("postgres", "secret") .property("hibernate.hbm2ddl.auto", "update") .createEntityManagerFactory(); } private static Long createStudent(SessionFactory sf, String name, int marks) { try (Session session = sf.openSession()) { Transaction tx = session.beginTransaction(); Student student = new Student(name, marks); session.persist(student); // INSERT tx.commit(); return student.getId(); } } private static Student findStudent(SessionFactory sf, Long id) { try (Session session = sf.openSession()) { return session.find(Student.class, id); // SELECT by primary key } } private static void updateMarks(SessionFactory sf, Long id, int newMarks) { try (Session session = sf.openSession()) { Transaction tx = session.beginTransaction(); Student student = session.find(Student.class, id); student.setMarks(newMarks); // no "update" call: dirty checking tx.commit(); // the UPDATE is sent here } } private static List<Student> findTopStudents(SessionFactory sf, int minMarks) { try (Session session = sf.openSession()) { return session.createQuery( "from Student s where s.marks >= :min order by s.marks desc", Student.class) .setParameter("min", minMarks) .getResultList(); } } private static void deleteStudent(SessionFactory sf, Long id) { try (Session session = sf.openSession()) { Transaction tx = session.beginTransaction(); Student student = session.find(Student.class, id); session.remove(student); // DELETE tx.commit(); } } } 

Output (the id may be different on your machine, and Hibernate also prints a few start-up log lines, which are normal):

Saved with id 1 Found: Student{id=1, name='Asha', marks=88} After update: Student{id=1, name='Asha', marks=95} Top students: [Student{id=1, name='Asha', marks=95}] Deleted student 1 

Let's go through the new parts:

  • HibernatePersistenceConfiguration is the modern way (Hibernate 7 and Jakarta Persistence 3.2) to configure Hibernate in plain Java code, with no XML file. You give it a name, the entity classes (managedClass), the JDBC URL, the credentials and any extra property.
  • hibernate.hbm2ddl.auto = update tells Hibernate to create or update tables to match your entities. It's handy for learning, but not for production (see the schema section later).
  • createEntityManagerFactory() builds and returns the SessionFactory. We use try-with-resources so it's closed when the program ends.
  • openSession() gives us a Session for one unit of work. Each method opens its own and closes it automatically.
  • persist, find, remove are the basic operations, and the update works through dirty checking: we changed a field of a managed object, and Hibernate wrote the UPDATE at commit().
  • The HQL query (from Student s where ...) is written against the entity and its fields (Student, s.marks), not the table and column names. The :min is a named parameter, bound safely.

Using Hibernate without Maven

"Without Maven" means you manage the library files yourself. You put the jars in a folder and tell Java where they are.

Step by step without Maven

1. Install the tools. You need a JDK 17 or newer, and PostgreSQL running with the school database (see the setup section).

2. Create the project folders.

 hibernate-demo/ |-- lib/ <- all the jar files go here |-- src/ | +-- com/example/ | |-- Student.java | +-- Main.java +-- out/ <- compiled classes will go here 

3. Get the jars into lib/. This is the hard part. hibernate-core alone isn't enough. It needs a set of other libraries (for example the Jakarta Persistence API, a logging library, a bytecode library, a parser library and a few more), and those may need others. You have two ways:

  • Option A, fully by hand: go to Maven Central, download hibernate-core-7.4.13.Final.jar, open its .pom file to read the dependency list, download each one, and repeat for those too. Then download the PostgreSQL driver postgresql-42.7.13.jar. Expect roughly 15 to 25 jars, and the exact list changes between versions.
  • Option B, use Maven only as a downloader: create a small file called fetch-pom.xml in the project folder:
<project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>tmp</groupId> <artifactId>fetch-jars</artifactId> <version>1</version> <dependencies> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>7.4.13.Final</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.7.13</version> </dependency> </dependencies> </project> 

Then run this once. It copies every needed jar, including the transitive ones, into lib/:

mvn -f fetch-pom.xml dependency:copy-dependencies -DoutputDirectory=lib 

After that, you can forget Maven and build with plain Java tools.

4. Add the code. Put the Student and Main classes from the previous section into src/com/example/.

5. Compile. The "lib/*" part adds every jar in the folder to the classpath:

mkdir -p out javac -cp "lib/*" -d out src/com/example/*.java 

6. Run. Linux and macOS use : to separate classpath entries, and Windows uses ;:

java -cp "out:lib/*" com.example.Main # Linux and macOS java -cp "out;lib/*" com.example.Main # Windows 

7. Check the result. You'll see the output from the main class section. You can also look in the database:

docker exec -it school-db psql -U postgres -d school -c "\dt" 
 Schema | Name | Type | Owner --------+---------+-------+---------- public | student | table | postgres 

What this approach costs you:

  • You track every jar and version by hand. Upgrading Hibernate means repeating step 3.
  • Two libraries needing different versions of a third is your problem to solve.
  • Every teammate must set up the same lib/ folder.

If you prefer an XML or properties file for configuration, Hibernate still supports hibernate.properties and hibernate.cfg.xml (read by calling new Configuration().configure()). The code-based approach above just avoids an extra file.

This is exactly why build tools exist, as we saw in the Maven post.

Using Hibernate with Maven

With Maven, you list the two libraries you need, and Maven fetches everything else.

Step by step with Maven

1. Create the project. (If you read my Maven post, this is the same command.)

mvn archetype:generate \ -DgroupId=com.example \ -DartifactId=hibernate-demo \ -DarchetypeArtifactId=maven-archetype-quickstart \ -DarchetypeVersion=1.5 \ -DinteractiveMode=false 

You can delete the generated App.java and the sample test, since we won't use them.

2. Edit pom.xml. Set the Java version and add the two dependencies:

<properties> <maven.compiler.release>17</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-core</artifactId> <version>7.4.13.Final</version> </dependency> <dependency> ...
Source:https://dev.to/mayur_bodhare/why-orms-exist-the-object-relational-mismatch-b2n