Hibernate is usually described as a library that makes it easy to map Java classes to relational database tables. But this formulation does no justice to the central role played by the relational data itself. So a better description might be:
Hibernate makes relational data visible to a program written in Java, in a natural and typesafe form,
-
making it easy to write complex queries and work with their results,
-
letting the program easily synchronize changes made in memory with the database, respecting the ACID properties of transactions, and
-
allowing performance optimizations to be made after the basic persistence logic has already been written.
Here the relational data is the focus, along with the importance of type safety. The goal of object/relational mapping (ORM) is to eliminate fragile and untypesafe code, and make large programs easier to maintain in the long run.
ORM takes the pain out of persistence by relieving the developer of the need to hand-write tedious, repetitive, and fragile code for flattening graphs of objects to database tables and rebuilding graphs of objects from flat SQL query result sets. Even better, ORM makes it much easier to tune performance later, after the basic persistence logic has already been written.
Tip
|
A perennial question is: should I use ORM, or plain SQL? The answer is usually: use both. JPA and Hibernate were designed to work in conjunction with handwritten SQL. You see, most programs with nontrivial data access logic will benefit from the use of ORM at least somewhere. But if Hibernate is making things more difficult, for some particularly tricky piece of data access logic, the only sensible thing to do is to use something better suited to the problem! Just because you’re using Hibernate for persistence doesn’t mean you have to use it for everything. |
Developers often ask about the relationship between Hibernate and JPA, so let’s take a short detour into some history.
Hibernate was the inspiration behind the Java (now Jakarta) Persistence API, or JPA, and includes a complete implementation of the latest revision of this specification.
The Hibernate project began in 2001, when Gavin King’s frustration with Entity Beans in EJB 2 boiled over. It quickly overtook other open source and commercial contenders to become the most popular persistence solution for Java, and the book Hibernate in Action, written with Christian Bauer, was an influential bestseller.
In 2004, Gavin and Christian joined a tiny startup called JBoss, and other early Hibernate contributors soon followed: Max Rydahl Andersen, Emmanuel Bernard, Steve Ebersole, and Sanne Grinovero.
Soon after, Gavin joined the EJB 3 expert group and convinced the group to deprecate Entity Beans in favor of a brand-new persistence API modelled after Hibernate. Later, members of the TopLink team got involved, and the Java Persistence API evolved as a collaboration between—primarily—Sun, JBoss, Oracle, and Sybase, under the leadership of Linda Demichiel.
Over the intervening two decades, many talented people have contributed to the development of Hibernate. We’re all especially grateful to Steve, who has led the project for many years, since Gavin stepped back to focus in other work.
We can think of the API of Hibernate in terms of three basic elements:
-
an implementation of the JPA-defined APIs, most importantly, of the interfaces
EntityManagerFactory
andEntityManager
, and of the JPA-defined O/R mapping annotations, -
a native API exposing the full set of available functionality, centered around the interfaces
SessionFactory
, which extendsEntityManagerFactory
, andSession
, which extendsEntityManager
, and -
a set of mapping annotations which augment the O/R mapping annotations defined by JPA, and which may be used with the JPA-defined interfaces, or with the native API.
Hibernate also offers a range of SPIs for frameworks and libraries which extend or integrate with Hibernate, but we’re not interested in any of that stuff here.
As an application developer, you must decide whether to:
-
write your program in terms of
Session
andSessionFactory
, or -
maximize portability to other implementations of JPA by, wherever reasonable, writing code in terms of
EntityManager
andEntityManagerFactory
, falling back to the native APIs only where necessary.
Whichever path you take, you will use the JPA-defined mapping annotations most of the time, and the Hibernate-defined annotations for more advanced mapping problems.
Tip
|
You might wonder if it’s possible to develop an application using only JPA-defined APIs, and, indeed, that’s possible in principle. JPA is a great baseline that really nails the basics of the object/relational mapping problem. But without the native APIs, and extended mapping annotations, you miss out on much of the power of Hibernate. |
Since Hibernate existed before JPA, and since JPA was modelled on Hibernate, we unfortunately have some competition and duplication in naming between the standard and native APIs. For example:
Hibernate | JPA |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Typically, the Hibernate-native APIs offer something a little extra that’s missing in JPA, so this isn’t exactly a flaw. But it’s something to watch out for.
If you’re completely new to Hibernate and JPA, you might already be wondering how the persistence-related code is structured.
Well, typically, our persistence-related code comes in two layers:
-
a representation of our data model in Java, which takes the form of a set of annotated entity classes, and
-
a larger number of functions which interact with Hibernate’s APIs to perform the persistence operations associated with our various transactions.
The first part, the data or "domain" model, is usually easier to write, but doing a great and very clean job of it will strongly affect your success in the second part.
Most people implement the domain model as a set of what we used to call "Plain Old Java Objects", that is, as simple Java classes with no direct dependencies on technical infrastructure, nor on application logic which deals with request processing, transaction management, communications, or interaction with the database.
Tip
|
Take your time with this code, and try to produce a Java model that’s as close as reasonable to the relational data model. Avoid using exotic or advanced mapping features when they’re not really needed.
When in the slightest doubt, map a foreign key relationship using |
There exists an extensive online literature which posits that there are rich domain models, where entities have methods implementing interesting business logic, and anemic domain models, where the entities are pure data holders, and that a developer should hold an opinion that one or the other of these sorts of domain model is "better".
We do not hold any such opinion, and if you ask us for one, we will most likely suddenly discover somewhere else we need to be.
A more interesting question is not how much logic belongs in the entity class, but what sort of logic belongs there. We think the answer is that an entity should never implement technical concerns, and should never obtain references to framework objects. Nor should it hold extra mutable state which is not very directly related to its role in representing persistent state. For example:
-
an entity may compute totals and averages, even caching them if necessary, enforce its invariants, interact with and construct other entities, and so on,
-
but the entity should never call the
EntityManager
or a Jakarta Data repository, build a criteria query, send a JMS message, start a transaction, publish events to the CDI event bus, maintain a stateful queue of events to be published later, or anything of a similar nature.
One way to summarize this is:
Entities do business logic; but they don’t do orchestration.
Later, we’ll discuss various ways to manage transactions, send event notifications, and query the database. Such code will always be external to the entity itself.
The second part of the code is much trickier to get right. This code must:
-
manage transactions and sessions,
-
interact with the database via the Hibernate session,
-
publish CDI events and send JMS messages,
-
fetch and prepare data needed by the UI, and
-
handle failures.
Tip
|
Responsibility for transaction and session management, and for recovery from certain kinds of failure, is best handled in some sort of framework code. |
We’re going to come back soon to the thorny question of how this persistence logic should be organized, and how it should fit into the rest of the system.
Before we get deeper into the weeds, we’ll quickly present a basic example program that will help you get started if you don’t already have Hibernate integrated into your project.
We begin with a simple gradle build file:
build.gradle
plugins {
id 'java'
}
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
// the GOAT ORM
implementation 'org.hibernate.orm:hibernate-core:{fullVersion}'
// Hibernate Processor
annotationProcessor 'org.hibernate.orm:hibernate-processor:{fullVersion}'
// Hibernate Validator
implementation 'org.hibernate.validator:hibernate-validator:8.0.1.Final'
implementation 'org.glassfish:jakarta.el:4.0.2'
// Agroal connection pool
runtimeOnly 'org.hibernate.orm:hibernate-agroal:{fullVersion}'
runtimeOnly 'io.agroal:agroal-pool:2.5'
// logging via Log4j
runtimeOnly 'org.apache.logging.log4j:log4j-core:2.24.1'
// H2 database
runtimeOnly 'com.h2database:h2:2.3.232'
}
Only the first of these dependencies is absolutely required to run Hibernate.
Next, we’ll add a logging configuration file for log4j:
log4j2.properties
rootLogger.level = info
rootLogger.appenderRefs = console
rootLogger.appenderRef.console.ref = console
logger.hibernate.name = org.hibernate.SQL
logger.hibernate.level = info
appender.console.name = console
appender.console.type = Console
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = %highlight{[%p]} %m%n
Now we need some Java code. We begin with our entity class:
Book.java
package org.hibernate.example;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
@Entity
class Book {
@Id
String isbn;
@NotNull
String title;
Book() {}
Book(String isbn, String title) {
this.isbn = isbn;
this.title = title;
}
}
Finally, let’s see code which configures and instantiates Hibernate and asks it to persist and query the entity. Don’t worry if this makes no sense at all right now. It’s the job of this Introduction to make all this crystal clear.
Main.java
package org.hibernate.example;
import org.hibernate.jpa.HibernatePersistenceConfiguration;
import static java.lang.System.out;
public class Main {
public static void main(String[] args) {
var sessionFactory =
new HibernatePersistenceConfiguration("Bookshelf")
.managedClass(Book.class)
// use H2 in-memory database
.jdbcUrl("jdbc:h2:mem:db1")
.jdbcCredentials("sa", "")
// set the Agroal connection pool size
.jdbcPoolSize(16)
// display SQL in console
.showSql(true, true, true)
.createEntityManagerFactory();
// export the inferred database schema
sessionFactory.getSchemaManager().create(true);
// persist an entity
sessionFactory.inTransaction(session -> {
session.persist(new Book("9781932394153", "Hibernate in Action"));
});
// query data using HQL
sessionFactory.inSession(session -> {
out.println(session.createSelectionQuery("select isbn||': '||title from Book").getSingleResult());
});
// query data using criteria API
sessionFactory.inSession(session -> {
var builder = sessionFactory.getCriteriaBuilder();
var query = builder.createQuery(String.class);
var book = query.from(Book.class);
query.select(builder.concat(builder.concat(book.get(Book_.isbn), builder.literal(": ")),
book.get(Book_.title)));
out.println(session.createSelectionQuery(query).getSingleResult());
});
}
}
In practice, we never access the database directly from a main()
method.
So now let’s talk about how to organize persistence logic in a real system.
The rest of this chapter is not compulsory.
If you’re itching for more details about Hibernate itself, you’re quite welcome to skip straight to the next chapter, and come back later.
In a real program, persistence logic like the code shown above is usually interleaved with other sorts of code, including logic:
-
implementing the rules of the business domain, or
-
for interacting with the user.
Therefore, many developers quickly—even too quickly, in our opinion—reach for ways to isolate the persistence logic into some sort of separate architectural layer. We’re going to ask you to suppress this urge for now.
We prefer a bottom-up approach to organizing our code. We like to start thinking about methods and functions, not about architectural layers and container-managed objects.
When we wrote An Introduction to Hibernate 6, the predecessor of this document, we broke with a long practice of remaining agnostic in debates over application architecture. Into the vacuum created by our agnosticism had poured a deluge of advice which tended to encourage over-engineering and violation of the First Commandment of software engineering: Don’t Repeat Yourself. We felt compelled to speak up for a more elementary approach.
Here, we reiterate our preference for design which emerges organically from the code itself, via a process of refactoring and iterative abstraction. The Extract Method refactoring is a far, far more powerful tool than drawing boxes and arrows on whiteboards.
In particular, we hereby give you permission to write code which mixes business logic with persistence logic within the same architectural layer. Every architectural layer comes with a high cost in boilerplate, and in many contexts a separate persistence layer is simply unnecessary. Both of the following architectures represent allowed points within the design space:
In the case that a separate persistence layer is helpful, we encourage you to consider the use of Jakarta Data repositories, in preference to older approaches.
To illustrate the sort of approach to code organization that we advocate, let’s consider a service which queries the database using HQL or SQL. We might start with something like this, a mix of UI and persistence logic:
@Path("/")
@Produces("application/json")
public class BookResource {
private final SessionFactory sessionfactory = .... ;
@GET
@Path("book/{isbn}")
public Book getBook(String isbn) {
var book = sessionFactory.fromTransaction(session -> session.find(Book.class, isbn));
return book == null ? Response.status(404).build() : book;
}
}
Indeed, we might also finish with something like that—it’s quite hard to identify anything concretely wrong with the code above, and for such a simple case it seems really difficult to justify making this code more complicated by introducing additional objects.
One very nice aspect of this code, which we wish to draw your attention to, is that session and transaction management is handled by generic "framework" code, just as we already recommended above.
In this case, we’re using the fromTransaction()
method, which happens to come built in to Hibernate.
But you might prefer to use something else, for example:
-
in a container environment like Jakarta EE or Quarkus, container-managed transactions and container-managed persistence contexts, or
-
something you write yourself.
The important thing is that calls like createEntityManager()
and getTransaction().begin()
don’t belong in regular program logic, because it’s tricky and tedious to get the error handling correct.
Let’s now consider a slightly more complicated case.
@Path("/")
@Produces("application/json")
public class BookResource {
private static final int RESULTS_PER_PAGE = 20;
private final SessionFactory sessionfactory = .... ;
@GET
@Path("books/{titlePattern}/{pageNumber:\\d+}")
public List<Book> findBooks(String titlePattern, int pageNumber) {
var page = Page.page(RESULTS_PER_PAGE, pageNumber);
var books =
sessionFactory.fromTransaction(session -> {
var findBooksByTitle = "from Book where title like ?1 order by title";
return session.createSelectionQuery(findBooksByTitle, Book.class)
.setParameter(1, titlePattern)
.setPage(page)
.getResultList();
});
return books.isEmpty() ? Response.status(404).build() : books;
}
}
This is fine, and we won’t complain if you prefer to leave the code exactly as it appears above. But there’s one thing we could perhaps improve. We love super-short methods with single responsibilities, and there looks to be an opportunity to introduce one here. Let’s hit the code with our favorite thing, the Extract Method refactoring. We obtain:
static List<Book> findBooksTitled(Session session, String titlePattern, Page page) {
var findBooksByTitle = "from Book where title like ?1 order by title";
return session.createSelectionQuery(findBooksByTitle, Book.class)
.setParameter(1, titlePattern)
.setPage(page)
.getResultList();
}
This is an example of a query method, a function which accepts arguments to the parameters of a HQL or SQL query, and executes the query, returning its results to the caller. And that’s all it does; it doesn’t orchestrate additional program logic, and it doesn’t perform transaction or session management.
It’s even better to specify the query string using the @NamedQuery
annotation, so that Hibernate can validate the query at startup time, that is, when the SessionFactory
is created, instead of when the query is first executed.
Indeed, since we included Hibernate Processor in our Gradle build, the query can even be validated at compile time.
We need a place to put the annotation, so let’s move our query method to a new class:
@CheckHQL // validate named queries at compile time
@NamedQuery(name = "findBooksByTitle",
query = "from Book where title like :title order by title")
class Queries {
static List<Book> findBooksTitled(Session session, String titlePattern, Page page) {
return session.createQuery(Queries_._findBooksByTitle_) //type safe reference to the named query
.setParameter("title", titlePattern)
.setPage(page)
.getResultList();
}
}
Notice that our query method doesn’t attempt to hide the EntityManager
from its clients.
Indeed, the client code is responsible for providing the EntityManager
or Session
to the query method.
The client code may:
-
obtain an
EntityManager
orSession
by callinginTransaction()
orfromTransaction()
, as we saw above, or, -
in an environment with container-managed transactions, it might obtain it via dependency injection.
Whatever the case, the code which orchestrates a unit of work usually just calls the Session
or EntityManager
directly, passing it along to helper methods like our query method if necessary.
@GET
@Path("books/{titlePattern}/{pageNumber:\\d+}")
public List<Book> findBooks(String titlePattern, int pageNumber) {
var page = Page.page(RESULTS_PER_PAGE, pageNumber);
var books =
sessionFactory.fromTransaction(session ->
// call handwritten query method
Queries.findBooksTitled(session, titlePattern, page));
return books.isEmpty() ? Response.status(404).build() : books;
}
You might be thinking that our query method looks a bit boilerplatey.
That’s true, perhaps, but we’re much more concerned that it’s still not perfectly typesafe.
Indeed, for many years, the lack of compile-time checking for HQL queries and code which binds arguments to query parameters was our number one source of discomfort with Hibernate.
Here, the @CheckHQL
annotation takes care of checking the query itself, but the call to setParameter()
is still not type safe.
Fortunately, there’s now a great solution to both problems. Hibernate Processor is able to fill in the implementation of such query methods for us. This facility is the topic of a whole chapter of this introduction, so for now we’ll just leave you with one simple example.
Suppose we simplify Queries
to just the following:
// a sort of proto-repository, this interface is never implemented
interface Queries {
// a HQL query method with a generated static "implementation"
@HQL("where title like :title order by title")
List<Book> findBooksTitled(String title, Page page);
}
Then Hibernate Processor automatically produces an implementation of the method annotated @HQL
in a class named Queries_
.
We can call it just like we were previously calling our handwritten version:
@GET
@Path("books/{titlePattern}/{pageNumber:\\d+}")
public List<Book> findBooks(String titlePattern, int pageNumber) {
var page = Page.page(RESULTS_PER_PAGE, pageNumber);
var books =
sessionFactory.fromTransaction(session ->
// call the generated query method "implementation"
Queries_.findBooksTitled(session, titlePattern, page));
return books.isEmpty() ? Response.status(404).build() : books;
}
In this case, the quantity of code eliminated is pretty trivial. The real value is in improved type safety. We now find out about errors in assignments of arguments to query parameters at compile time.
This is all quite nice so far, but at this point you’re probably wondering whether we could use dependency injection to obtain an instance of the Queries
interface, and have this object take care of obtaining its own Session
.
Well, indeed we can.
What we need to do is indicate the kind of session the Queries
interface depends on, by adding a method to retrieve the session.
Observe, again, that we’re still not attempting to hide the Session
from the client code.
// a true repository interface with generated implementation
interface Queries {
// declare the kind of session backing this repository
Session session();
// a HQL query method with a generated implementation
@HQL("where title like :title order by title")
List<Book> findBooksTitled(String title, Page page);
}
The Queries
interface is now considered a repository, and we may use CDI to inject the repository implementation generated by Hibernate Processor.
Also, since I guess we’re now working in some sort of container environment, we’ll let the container manage transactions for us.
@Inject Queries queries; // inject the repository
@GET
@Path("books/{titlePattern}/{pageNumber:\\d+}")
@Transactional
public List<Book> findBooks(String titlePattern, int pageNumber) {
var page = Page.page(RESULTS_PER_PAGE, pageNumber);
var books = queries.findBooksTitled(session, titlePattern, page); // call the repository method
return books.isEmpty() ? Response.status(404).build() : books;
}
Alternatively, if CDI isn’t available, we may directly instantiate the generated repository implementation class using new Queries_(entityManager)
.
Tip
|
The Jakarta Data specification now formalizes this approach using standard annotations, and our implementation of this specification, Hibernate Data Repositories, is built into Hibernate Processor. You probably already have it available in your program. Unlike other repository frameworks, Hibernate Data Repositories offers something that plain JPA simply doesn’t have: full compile-time type safety for your queries. To learn more, please refer to Introducing Hibernate Data Repositories. |
At the time we wrote An Introduction to Hibernate 6, we were especially frustrated with the limitations of popular frameworks which claimed to simplify the use of JPA by wrapping and hiding the EntityManager
.
In our considered opinion, such frameworks typically made JPA harder to use, sometimes misleading users into misuse of the technology.
The birth of the Jakarta Data specification has obsoleted our arguments against repositories, along with the older frameworks which were the source of our frustration.
Jakarta Data—as realized by Hibernate Data Repositories—offers a clean but very flexible way to organize code, along with much better compile-time type safety, without getting in the way of direct use of the StatelessSession
.
Now that we have a rough picture of what our persistence logic might look like, it’s natural to ask how we should test our code.
When we write tests for our persistence logic, we’re going to need:
-
a database, with
-
an instance of the schema mapped by our persistent entities, and
-
a set of test data, in a well-defined state at the beginning of each test.
It might seem obvious that we should test against the same database system that we’re going to use in production, and, indeed, we should certainly have at least some tests for this configuration. But on the other hand, tests which perform I/O are much slower than tests which don’t, and most databases can’t be set up to run in-process.
So, since most persistence logic written using Hibernate 6 is extremely portable between databases, it often makes good sense to test against an in-memory Java database. (H2 is the one we recommend.)
Caution
|
We do need to be careful here if our persistence code uses native SQL, or if it uses concurrency-management features like pessimistic locks. |
Whether we’re testing against our real database, or against an in-memory Java database, we’ll need to export the schema at the beginning of a test suite.
We usually do this when we create the Hibernate SessionFactory
or JPA EntityManagerFactory
, and so traditionally we’ve used a configuration property for this.
The JPA-standard property is jakarta.persistence.schema-generation.database.action
.
For example, if we’re using PersistenceConfiguration
to configure Hibernate, we could write:
configuration.property(PersistenceConfiguration.SCHEMAGEN_DATABASE_ACTION,
Action.SPEC_ACTION_DROP_AND_CREATE);
Alternatively, we may use the new SchemaManager
API to export the schema, just as we did above.
This option is especially convenient when writing tests.
sessionFactory.getSchemaManager().create(true);
Since executing DDL statements is very slow on many databases, we don’t want to do this before every test. Instead, to ensure that each test begins with the test data in a well-defined state, we need to do two things before each test:
-
clean up any mess left behind by the previous test, and then
-
reinitialize the test data.
We may truncate all the tables, leaving an empty database schema, using the SchemaManager
.
sessionFactory.getSchemaManager().truncate();
After truncating tables, we might need to initialize our test data. We may specify test data in a SQL script, for example:
insert into Books (isbn, title) values ('9781932394153', 'Hibernate in Action')
insert into Books (isbn, title) values ('9781932394887', 'Java Persistence with Hibernate')
insert into Books (isbn, title) values ('9781617290459', 'Java Persistence with Hibernate, Second Edition')
If we name this file import.sql
, and place it in the root classpath, that’s all we need to do.
Otherwise, we need to specify the file in the configuration property jakarta.persistence.sql-load-script-source
.
If we’re using PersistenceConfiguration
to configure Hibernate, we could write:
configuration.property(AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE,
"/org/example/test-data.sql");
The SQL script will be executed every time export()
or truncate()
is called.
Tip
|
There’s another sort of mess a test can leave behind: cached data in the second-level cache. We recommend disabling Hibernate’s second-level cache for most sorts of testing. Alternatively, if the second-level cache is not disabled, then before each test we should call: sessionFactory.getCache().evictAllRegions(); |
Now, suppose you’ve followed our advice, and written your entities and query methods to minimize dependencies on "infrastructure", that is, on libraries other than JPA and Hibernate, on frameworks, on container-managed objects, and even on bits of your own system which are hard to instantiate from scratch. Then testing persistence logic is now straightforward!
You’ll need to:
-
bootstrap Hibernate and create a
SessionFactory
orEntityManagerFactory
at the beginning of your test suite (we’ve already seen how to do that), and -
create a new
Session
orEntityManager
inside each@Test
method, usinginTransaction()
, for example.
Actually, some tests might require multiple sessions. But be careful not to leak a session between different tests.
Tip
|
Another important test we’ll need is one which validates our O/R mapping annotations against the actual database schema. This is again the job of the schema management tooling, either: configuration.property(PersistenceConfiguration.SCHEMAGEN_DATABASE_ACTION,
Action.ACTION_VALIDATE); Or: sessionFactory.getSchemaManager().validate(); This "test" is one which many people like to run even in production, when the system starts up. |
It’s now time to begin our journey toward actually understanding the code we saw earlier.
This introduction will guide you through the basic tasks involved in developing a program that uses Hibernate for persistence:
-
configuring and bootstrapping Hibernate, and obtaining an instance of
SessionFactory
orEntityManagerFactory
, -
writing a domain model, that is, a set of entity classes which represent the persistent types in your program, and which map to tables of your database,
-
customizing these mappings when the model maps to a pre-existing relational schema,
-
using the
Session
orEntityManager
to perform operations which query the database and return entity instances, or which update the data held in the database, -
using Hibernate Processor to improve compile-time type-safety,
-
writing complex queries using the Hibernate Query Language (HQL) or native SQL, and, finally
-
tuning performance of the data access logic.
Naturally, we’ll start at the top of this list, with the least-interesting topic: configuration.