Movatterモバイル変換


[0]ホーム

URL:


A Short Guide to Hibernate 7

version 7.0.10.Final
Table of Contents

Preface

Hibernate 6 was a major redesign of the world’s most popular and feature-rich ORM solution.The redesign touched almost every subsystem of Hibernate, including the APIs, mapping annotations, and the query language.This new Hibernate was suddenly more powerful, more robust, more portable, and more type safe.

Hibernate 7 builds on this foundation, adds support forJPA 3.2, and introducesHibernate Data Repositories, an implementation of theJakarta Data specification.Taken together, these enhancements yield a level of compile-time type safety—​and resulting developer productivity—​which was previously impossible.Hibernate Data Repositories offers truly seamless integration of the ORM solution with the persistence layer, obsoleting older add-on repository frameworks.

Hibernate ORM andHibernate Reactive are core components ofQuarkus 3, the most exciting new environment for cloud-native development in Java, and Hibernate remains the persistence solution of choice for almost every major Java framework or server.

Unfortunately, the changes in Hibernate 6 also obsoleted much of the information about Hibernate that’s available in books, in blog posts, and on stackoverflow.

This guide is an up-to-date, high-level discussion of the current feature set and recommended usage.It does not attempt to cover every feature and should be used in conjunction with other documentation:

The Hibernate User Guide includes detailed discussions of most aspects of Hibernate.But with so much information to cover, readability is difficult to achieve, and so it’s most useful as a reference.Where necessary, we’ll provide links to relevant sections of the User Guide.

1. Introduction

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 makesrelational data visible to a program written in Java, in anatural andtypesafe form,

  1. making it easy to write complex queries and work with their results,

  2. letting the program easily synchronize changes made in memory with the database, respecting the ACID properties of transactions, and

  3. 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 ofobject/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.

A perennial question is: should I use ORM, or plain SQL?The answer is usually:use both.JPA and Hibernate were designed to workin conjunction with handwritten SQL.You see, most programs with nontrivial data access logic will benefit from the use of ORM at leastsomewhere.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 foreverything.

Let’s underline the important point here: the goal of ORM isnot to hide SQL or the relational model.After all, Hibernate’s query language isnothing more than an object-oriented dialect of ANSI SQL.

Is Hibernate a "leaky abstraction"?

Hibernate—​and ORM in general—​has been accused of being aleaky abstraction, that is, of failing to completely hide the underlying relational database.Is this true?Well, as you can guess from what we’ve already said, the short answer is: yes,and that’s a good thing.Of course, whether you consider an abstraction "leaky" depends on what you think it’s trying to abstract.Hibernate successfully abstracts many things: variations between dialects of SQL, messy interactions with JDBC, SQL object naming conventions, and so on.But it doesn’t even attempt to pretend there’s anything other than a relational data model underlying all this.The simple reason is performance. It’s just not possible toachieve acceptable performance in data access without acknowledging the nature of the underlying persistent representation.We’re old enough to have seen multiple generations of developer relearn this lesson by experience.

Developers often ask about the relationship between Hibernate and JPA, so let’s take a short detour into some history.

1.1. Hibernate and JPA

Hibernate was the inspiration behind theJava (nowJakarta)Persistence API, or JPA, and includes a complete implementation of the latest revision ofthis specification.

The early history of Hibernate and JPA

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 bookHibernate 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 interfacesEntityManagerFactory andEntityManager, and of the JPA-defined O/R mapping annotations,

  • anative API exposing the full set of available functionality, centered around the interfacesSessionFactory, which extendsEntityManagerFactory, andSession, which extendsEntityManager, and

  • a set ofmapping 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.

API overview

As an application developer, you must decide whether to:

  • write your program in terms ofSession andSessionFactory, or

  • maximize portability to other implementations of JPA by, wherever reasonable, writing code in terms ofEntityManager 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.

You might wonder if it’s possible to develop an application usingonly 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:

Table 1. Examples of competing APIs with similar naming
HibernateJPA

org.hibernate.annotations.CascadeType

javax.persistence.CascadeType

org.hibernate.FlushMode

javax.persistence.FlushModeType

org.hibernate.annotations.FetchMode

javax.persistence.FetchType

org.hibernate.query.Query

javax.persistence.Query

org.hibernate.Cache

javax.persistence.Cache

@org.hibernate.annotations.NamedQuery

@javax.persistence.NamedQuery

@org.hibernate.annotations.Cache

@javax.persistence.Cacheable

org.hibernate.relational.SchemaManager

jakarta.persistence.SchemaManager

Typically, the Hibernate-native APIs offer something a little extra that’s missing in JPA, so this isn’t exactly aflaw.But it’s something to watch out for.

1.2. Writing Java code with Hibernate

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:

  1. a representation of our data model in Java, which takes the form of a set of annotated entity classes, and

  2. 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.

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@ManyToOne with@OneToMany(mappedBy=…​) in preference to more complicated association mappings.

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.

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 tocome 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.

1.3. Hello, Hibernate

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 simpleGradle build file:

build.gradle
plugins{id'java'}group='org.example'version='1.0-SNAPSHOT'repositories{mavenCentral()}dependencies{// the GOAT ORMimplementation'org.hibernate.orm:hibernate-core:7.0.10.Final'// Hibernate ProcessorannotationProcessor'org.hibernate.orm:hibernate-processor:7.0.10.Final'// Hibernate Validatorimplementation'org.hibernate.validator:hibernate-validator:8.0.1.Final'implementation'org.glassfish:jakarta.el:4.0.2'// Agroal connection poolruntimeOnly'org.hibernate.orm:hibernate-agroal:7.0.10.Final'runtimeOnly'io.agroal:agroal-pool:2.5'// logging via Log4jruntimeOnly'org.apache.logging.log4j:log4j-core:2.24.1'// H2 databaseruntimeOnly'com.h2database:h2:2.3.232'}

Only the first of these dependencies is absolutelyrequired to run Hibernate.

Next, we’ll add a logging configuration file forlog4j:

log4j2.properties
rootLogger.level=inforootLogger.appenderRefs=consolerootLogger.appenderRef.console.ref=console# SQL statements (set level=debug to enable)logger.hibernate.name=org.hibernate.SQLlogger.hibernate.level=info# JDBC parameter binding (set level=trace to enable)logger.jdbc-bind.name=org.hibernate.orm.jdbc.bindlogger.jdbc-bind.level=info# JDBC result set extraction (set level=trace to enable)logger.jdbc-extract.name=org.hibernate.orm.jdbc.extractlogger.jdbc-extract.level=info# JDBC batching (set level=trace to enable)logger.batch.name=org.hibernate.orm.jdbc.batchlogger.batch.level=info# direct log output to the consoleappender.console.name=consoleappender.console.type=Consoleappender.console.layout.type=PatternLayoutappender.console.layout.pattern=%highlight{[%p]} %m%n

Now we need some Java code.We begin with ourentity class:

Book.java
packageorg.hibernate.example;importjakarta.persistence.Entity;importjakarta.persistence.Id;importjakarta.validation.constraints.NotNull;@EntityclassBook{@IdStringisbn;@NotNullStringtitle;Book(){}Book(Stringisbn,Stringtitle){this.isbn=isbn;this.title=title;}}

Finally, let’s see code whichconfigures and instantiates Hibernate and asks it topersist and query the entity.Don’t worry if this makes no sense at all right now.It’s the job of the rest of this Short Guide to make all this crystal clear.

Main.java
packageorg.hibernate.example;importorg.hibernate.jpa.HibernatePersistenceConfiguration;importstaticjava.lang.System.out;publicclassMain{publicstaticvoidmain(String[]args){varsessionFactory=newHibernatePersistenceConfiguration("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 schemasessionFactory.getSchemaManager().create(true);// persist an entitysessionFactory.inTransaction(session->{session.persist(newBook("9781932394153","Hibernate in Action"));});// query data using HQLsessionFactory.inSession(session->{out.println(session.createSelectionQuery("select isbn||': '||title from Book").getSingleResult());});// query data using criteria APIsessionFactory.inSession(session->{varbuilder=sessionFactory.getCriteriaBuilder();varquery=builder.createQuery(String.class);varbook=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 amain() 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 thenext chapter, and come back later.

1.4. Entities

A class in the domain model which directly represents a relational database table is called anentity.Entity classes are central to object persistence and to object/relational mapping.They’re also, typically, central players in the business logic of our application program.Entities represent thethings in our business domain.This makes them very important objects indeed!

Given how much weight an entity already bears due to its very nature, we need to think carefully before weighing it down with too many additional responsibilities.

What sort of logic belongs in an entity?

There exists an extensive online literature which posits that there arerich domain models, where entities have methods implementing interesting business logic, andanemic 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 nothow much logic belongs in the entity class, butwhat 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, and its annotations express how it maps to database tables,

  • but the entity should not call theEntityManager 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 tomanage transactions,send event notifications, andquery the database.Such code will always be external to the entity itself.

In keeping with our commitment to anti-dogmatism, we would like to add the following important caveat to the discussion in the previous callout.

Active Record

The discussionabove expresses our "traditional" approach—​which lay behind the design of Hibernate, of JPA, and of Jakarta Data—​where entity classes are plain Java objects without dependence on framework code.An alternative approach is the Active Record pattern, as exemplified byPanache.In Active Record, entity types inherit framework objects, and persistence operations are located directly on the entities.You can think of this as merging the roles of entity and DAO/Repository into a single object.

Active Record comes with both upsides and downsides, but we don’t mean to exclude it from consideration.We must therefore slightly modify the prescription we’ve given above: in an Active Record, it’s obviously OK to access theEntityManager and perform other persistence-related operations, and we therefore expect our Active Record class to look somewhat more "technical" than a trad entity.

But the basic principle remains intact: an entity does not do orchestration, it does not manage transactions, it does not obtain references toother sorts of framework object, and it does not hold mutable state unrelated to its persistent state.

For now, we’re going to assume that entities are implemented as plain Java classes.

1.5. Stateful and stateless sessions

It should be very clear from the example codeabove, that the session is also a very important object.It exposes basic operations likepersist() andcreateQuery(), and so it’s our first port of call when we want tointeract with the database via Hibernate.In the code we just saw, we’ve used astateful session.

Later, we’ll learn about the idea of apersistence context.Oversimplifying for now, you can think of it as a cache of data which has been read in the current transaction.Thus, in the architecture of Hibernate, it’s sometimes called thefirst-level cache.Each stateful session — that is, every HibernateSession, and every JPAEntityManager — has its own persistence context.

But stateful sessions have never been the only possibility.TheStatelessSession interface offers a way to interact with Hibernatewithout going through a persistence context.However, the programming model is somewhat different.

Stateless sessions

Among our biggest regrets is that we didn’t give enough love toStatelessSession twenty years ago.Sure, a stateful session is in some sense more powerful, or at least more magical.But with that magic comes a loss of direct control over persistence operations, and some traps for inexperienced users.A significant minority of developers find working with a persistence context frustrating, and they would surely be better served by a stateless session.

  • We used to viewStatelessSession as an API directed toward very specific usage patterns, in particular, batch processing of large numbers of entities.As a result, we left out certain functionality — for example, use of thesecond-level cache — which didn’t seem relevant to those use cases.This leftStatelessSession lacking feature parity withSession, and it was a mistake.In Hibernate 7, we’ve fixed this mistake.AStatelessSession now offers essentially all the functionality of Hibernate except, naturally, the first-level cache.

  • Compounding our error, we leftStatelessSession out of JPA.This meant that a large number of Hibernate usersdidn’t even realize this option existed.We promise to make sure there are stateless sessions in Jakarta Persistence 4.

So, finally, let us state for the record: we messed up here.Hibernate is all aboutobject/relational mapping; persistence contexts are something extra on top.You don’t have to use stateful sessions, and you’re not doing anything wrong if you decide to use stateless sessions instead.

As of Hibernate 7, a key decision for any new project is which of these programming models to take as a baseline.Fortunately, the two models aren’t mutually exclusive.This is a friendly competition, where the two APIs are designed to complement each other.Even if we decide to useStatefulSession most of the time, we can still use aStatelessSession wherever it’s more convenient.

On the other hand, if you decide to adopt Jakarta Data, the decision is made for you: repositories in Jakarta Data 1.0 are always stateless, and inHibernate Data Repositories a repository is backed by aStatelessSession.

But now we’ve got just a little bit ahead of ourselves.In the next section taking we’re taking a journey whichmight — but definitely doesn’tnecessarily — end at the idea of a "repository".

1.6. Organizing persistence logic

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—eventoo 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 abottom-up approach to organizing our code.We like to start thinking about methods and functions, not about architectural layers and container-managed objects.

Rethinking the persistence layer

When we wroteAn 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:

API overview

In the case that a separate persistence layeris 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")publicclassBookResource{privatefinalSessionFactorysessionfactory=....;@GET@Path("book/{isbn}")publicBookgetBook(Stringisbn){varbook=sessionFactory.fromTransaction(session->session.find(Book.class,isbn));returnbook==null?Response.status(404).build():book;}}

Indeed, we might alsofinish 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 thefromTransaction() 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 andcontainer-managed persistence contexts, or

  • something you write yourself.

The important thing is that calls likecreateEntityManager() andgetTransaction().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")publicclassBookResource{privatestaticfinalintRESULTS_PER_PAGE=20;privatefinalSessionFactorysessionfactory=....;@GET@Path("books/{titlePattern}/{pageNumber:\\d+}")publicList<Book>findBooks(StringtitlePattern,intpageNumber){varpage=Page.page(RESULTS_PER_PAGE,pageNumber);varbooks=sessionFactory.fromTransaction(session->{varfindBooksByTitle="from Book where title like ?1 order by title";returnsession.createSelectionQuery(findBooksByTitle,Book.class).setParameter(1,titlePattern).setPage(page).getResultList();});returnbooks.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:

staticList<Book>findBooksTitled(Sessionsession,StringtitlePattern,Pagepage){varfindBooksByTitle="from Book where title like ?1 order by title";returnsession.createSelectionQuery(findBooksByTitle,Book.class).setParameter(1,titlePattern).setPage(page).getResultList();}

This is an example of aquery 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 theSessionFactory is created, instead of when the query is first executed.Indeed, since we includedHibernate Processor in ourGradle build, the query can even be validated atcompile 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")classQueries{staticList<Book>findBooksTitled(Sessionsession,StringtitlePattern,Pagepage){returnsession.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 theEntityManager from its clients.Indeed, the client code is responsible for providing theEntityManager orSession to the query method.

The client code may:

  • obtain anEntityManager 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 theSession orEntityManager directly, passing it along to helper methods like our query method if necessary.

@GET@Path("books/{titlePattern}/{pageNumber:\\d+}")publicList<Book>findBooks(StringtitlePattern,intpageNumber){varpage=Page.page(RESULTS_PER_PAGE,pageNumber);varbooks=sessionFactory.fromTransaction(session->// call handwritten query methodQueries.findBooksTitled(session,titlePattern,page));returnbooks.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 tosetParameter() 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 ofa whole chapter of this introduction, so for now we’ll just leave you with one simple example.

Suppose we simplifyQueries to just the following:

// a sort of proto-repository, this interface is never implementedinterfaceQueries{// a HQL query method with a generated static "implementation"@HQL("where title like :title order by title")List<Book>findBooksTitled(Stringtitle,Pagepage);}

Then Hibernate Processor automatically produces an implementation of the method annotated@HQL in a class namedQueries_.We can call it just like we were previously calling our handwritten version:

@GET@Path("books/{titlePattern}/{pageNumber:\\d+}")publicList<Book>findBooks(StringtitlePattern,intpageNumber){varpage=Page.page(RESULTS_PER_PAGE,pageNumber);varbooks=sessionFactory.fromTransaction(session->// call the generated query method "implementation"Queries_.findBooksTitled(session,titlePattern,page));returnbooks.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 aninstance of theQueries interface, and have this object take care of obtaining its ownSession.Well, indeed we can.What we need to do is indicate the kind of session theQueries interface depends on, by adding a method to retrieve the session.Observe, again, that we’restill not attempting to hide theSession from the client code.

// a true repository interface with generated implementationinterfaceQueries{// declare the kind of session backing this repositorySessionsession();// a HQL query method with a generated implementation@HQL("where title like :title order by title")List<Book>findBooksTitled(Stringtitle,Pagepage);}

TheQueries interface is now considered arepository, 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.

@InjectQueriesqueries;// inject the repository@GET@Path("books/{titlePattern}/{pageNumber:\\d+}")@TransactionalpublicList<Book>findBooks(StringtitlePattern,intpageNumber){varpage=Page.page(RESULTS_PER_PAGE,pageNumber);varbooks=queries.findBooksTitled(session,titlePattern,page);// call the repository methodreturnbooks.isEmpty()?Response.status(404).build():books;}

Alternatively, if CDI isn’t available, we may directly instantiate the generated repository implementation class usingnew Queries_(entityManager).

TheJakarta Data specification now formalizes this approach using standard annotations, and our implementation of this specification, Hibernate Data Repositories, is built intoHibernate 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 toIntroducing Hibernate Data Repositories.

Why we changed our mind about repositories

At the time we wroteAn 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 theEntityManager.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 theStatelessSession.

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.

1.7. Testing persistence logic

When we write tests for our persistence logic, we’re going to need:

  1. a database, with

  2. an instance of the schema mapped by our persistent entities, and

  3. 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 leastsome 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 isextremely portable between databases, it often makes good sense to test against an in-memory Java database.(H2 is the one we recommend.)

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.Weusually do this when we create the HibernateSessionFactory or JPAEntityManagerFactory, and so traditionally we’ve used aconfiguration property for this.

The JPA-standard property isjakarta.persistence.schema-generation.database.action.For example, if we’re usingPersistenceConfiguration to configure Hibernate, we could write:

configuration.property(PersistenceConfiguration.SCHEMAGEN_DATABASE_ACTION,Action.SPEC_ACTION_DROP_AND_CREATE);

Alternatively, we may use the newSchemaManager API to export the schema, just as we didabove.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:

  1. clean up any mess left behind by the previous test, and then

  2. reinitialize the test data.

We may truncate all the tables, leaving an empty database schema, using theSchemaManager.

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:

/import.sql
insertintoBooks(isbn,title)values('9781932394153','Hibernate in Action')insertintoBooks(isbn,title)values('9781932394887','Java Persistence with Hibernate')insertintoBooks(isbn,title)values('9781617290459','Java Persistence with Hibernate, Second Edition')

If we name this fileimport.sql, and place it in the root classpath, that’s all we need to do.

Otherwise, we need to specify the file in theconfiguration propertyjakarta.persistence.sql-load-script-source.If we’re usingPersistenceConfiguration 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 timeexport() ortruncate() is called.

There’s another sort of mess a test can leave behind: cached data in thesecond-level cache.We recommenddisabling 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 aSessionFactory orEntityManagerFactory at the beginning of your test suite (we’ve already seen how to do that), and

  • create a newSession 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.

Another important test we’ll need is one which validates ourO/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.

1.8. Overview

It’s now time to begin our journey toward actuallyunderstanding the code we saw earlier.

This introduction will guide you through the basic tasks involved in developing a program that uses Hibernate for persistence:

  1. configuring and bootstrapping Hibernate, and obtaining an instance ofSessionFactory orEntityManagerFactory,

  2. writing adomain model, that is, a set ofentity classes which represent the persistent types in your program, and which map to tables of your database,

  3. customizing these mappings when the model maps to a pre-existing relational schema,

  4. using theSession orEntityManager to perform operations which query the database and return entity instances, or which update the data held in the database,

  5. using Hibernate Processor to improve compile-time type-safety,

  6. writing complex queries using the Hibernate Query Language (HQL) or native SQL, and, finally

  7. tuning performance of the data access logic.

Naturally, we’ll start at the top of this list, with the least-interesting topic:configuration.

2. Configuration and bootstrap

We would love to make this section short.Unfortunately, there are several distinct ways to configure and bootstrap Hibernate, and we’re going to have to describe at least two of them in detail.

The five basic ways to obtain an instance of Hibernate are shown in the following table:

Using the standard JPA-defined XML, and the operationPersistence.createEntityManagerFactory()

Usually chosen when portability between JPA implementations is important.

Using the standard JPA-definedPersistenceConfiguration class

Usually chosen when portability between JPA implementations is important, but programmatic control is desired.

UsingHibernatePersistenceConfiguration or the olderConfiguration class to construct aSessionFactory

When portability between JPA implementations is not important, this option adds some convenience and saves a typecast.

Using the more complex APIs defined inorg.hibernate.boot

Used primarily by framework integrators, this option is outside the scope of this document.

By letting the container take care of the bootstrap process and of injecting theSessionFactory orEntityManagerFactory

Used in a container environment like WildFly or Quarkus.

Here we’ll focus on the first two options.

Hibernate in containers

Actually, the last option is extremely popular, since every major Java application server and microservice framework comes with built-in support for Hibernate.Such container environments typically also feature facilities to automatically manage the lifecycle of anEntityManager orSession and its association with container-managed transactions.

To learn how to configure Hibernate in such a container environment, you’ll need to refer to the documentation of your chosen container.For Quarkus, here’s therelevant documentation.

If you’re using Hibernate outside of a container environment,you’ll need to:

  • include Hibernate ORM itself, along with the appropriate JDBC driver, as dependencies of your project, and

  • configure Hibernate with information about your database,by specifying configuration properties.

2.1. Including Hibernate in your project build

First, add the following dependency to your project:

org.hibernate.orm:hibernate-core:{version}

Where{version} is the version of Hibernate you’re using,7.0.10.Final, for example.

You’ll also need to add a dependency for the JDBCdriver for your database.

Table 2. JDBC driver dependencies
DatabaseDriver dependency

PostgreSQL or CockroachDB

org.postgresql:postgresql:{version}

MySQL or TiDB

com.mysql:mysql-connector-j:{version}

MariaDB

org.mariadb.jdbc:mariadb-java-client:{version}

DB2

com.ibm.db2:jcc:{version}

SQL Server

com.microsoft.sqlserver:mssql-jdbc:{version}

Oracle

com.oracle.database.jdbc:ojdbc17:{version}

H2

com.h2database:h2:{version}

HSQLDB

org.hsqldb:hsqldb:{version}

Where{version} is the latest version of the JDBC driver for your database.

2.2. Optional dependencies

Optionally, you might also add any of the following additional features:

Table 3. Optional dependencies
Optional featureDependencies

AnSLF4J logging implementation

org.apache.logging.log4j:log4j-core
ororg.slf4j:slf4j-jdk14

A JDBC connection pool, for example,Agroal

org.hibernate.orm:hibernate-agroal
andio.agroal:agroal-pool

TheHibernate Processor, especially if you’re using Jakarta Data or the JPA criteria query API

org.hibernate.orm:hibernate-processor

TheQuery Validator, for compile-time checking of HQL

org.hibernate:query-validator

Hibernate Validator, an implementation ofBean Validation

org.hibernate.validator:hibernate-validator
andorg.glassfish:jakarta.el

Local second-level cache support via JCache andEHCache

org.hibernate.orm:hibernate-jcache
andorg.ehcache:ehcache

Local second-level cache support via JCache andCaffeine

org.hibernate.orm:hibernate-jcache
andcom.github.ben-manes.caffeine:jcache

Distributed second-level cache support viaInfinispan

org.infinispan:infinispan-hibernate-cache-v60

A JSON serialization library for working with JSON datatypes, for example,Jackson orYasson

com.fasterxml.jackson.core:jackson-databind
ororg.eclipse:yasson

Hibernate Spatial

org.hibernate.orm:hibernate-spatial

Envers, for auditing historical data

org.hibernate.orm:hibernate-envers

Hibernate JFR, for monitoring via Java Flight Recorder

org.hibernate.orm:hibernate-jfr

You might also add the Hibernatebytecode enhancer to yourGradle build if you want to usefield-level lazy fetching.

2.3. Configuration using JPA XML

Sticking to the JPA-standard approach, we would provide a file namedpersistence.xml, which we usually place in theMETA-INF directory of apersistence archive, that is, of the.jar file or directory which contains our entity classes.

<persistencexmlns="http://java.sun.com/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/persistence https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd"version="2.0"><persistence-unitname="org.hibernate.example"><class>org.hibernate.example.Book</class><class>org.hibernate.example.Author</class><properties><!-- PostgreSQL --><propertyname="jakarta.persistence.jdbc.url"value="jdbc:postgresql://localhost/example"/><!-- Credentials --><propertyname="jakarta.persistence.jdbc.user"value="gavin"/><propertyname="jakarta.persistence.jdbc.password"value="hibernate"/><!-- Automatic schema export --><propertyname="jakarta.persistence.schema-generation.database.action"value="drop-and-create"/><!-- SQL statement logging --><propertyname="hibernate.show_sql"value="true"/><propertyname="hibernate.format_sql"value="true"/><propertyname="hibernate.highlight_sql"value="true"/></properties></persistence-unit></persistence>

The<persistence-unit> element defines a namedpersistence unit, that is:

  • a collection of associated entity types, along with

  • a set of default configuration settings, which may be augmented or overridden at runtime.

Each<class> element specifies the fully-qualified name of an entity class.

Scanning for entity classes

In some container environments, for example, in any EE container, the<class> elements are unnecessary, since the container will scan the archive for annotated classes, and automatically recognize any class annotated@Entity.

Each<property> element specifies aconfiguration property and its value.Note that:

  • the configuration properties in thejakarta.persistence namespace are standard properties defined by the JPA spec, and

  • properties in thehibernate namespace are specific to Hibernate.

We may obtain anEntityManagerFactory by callingPersistence.createEntityManagerFactory():

EntityManagerFactoryentityManagerFactory=Persistence.createEntityManagerFactory("org.hibernate.example");

If necessary, we may override configuration properties specified inpersistence.xml:

EntityManagerFactoryentityManagerFactory=Persistence.createEntityManagerFactory("org.hibernate.example",Map.of(AvailableSettings.JAKARTA_JDBC_PASSWORD,password));

2.4. Programmatic configuration using JPA API

The newPersistenceConfiguration class allows full programmatic control over creation of theEntityManagerFactory.

EntityManagerFactoryentityManagerFactory=newPersistenceConfiguration("Bookshop").managedClass(Book.class).managedClass(Author.class)// PostgreSQL.property(PersistenceConfiguration.JDBC_URL,"jdbc:postgresql://localhost/example")// Credentials.property(PersistenceConfiguration.JDBC_USER,user).property(PersistenceConfiguration.JDBC_PASSWORD,password)// Automatic schema export.property(PersistenceConfiguration.SCHEMAGEN_DATABASE_ACTION,Action.SPEC_ACTION_DROP_AND_CREATE)// SQL statement logging.property(JdbcSettings.SHOW_SQL,true).property(JdbcSettings.FORMAT_SQL,true).property(JdbcSettings.HIGHLIGHT_SQL,true)// Create a new EntityManagerFactory.createEntityManagerFactory();

The specification gives JPA implementors like Hibernate explicit permission to extend this class, and so Hibernate offers theHibernatePersistenceConfiguration, which lets us obtain aSessionFactory without any need for a cast.

SessionFactorysessionFactory=newHibernatePersistenceConfiguration("Bookshop").managedClass(Book.class).managedClass(Author.class)// PostgreSQL.jdbcUrl("jdbc:postgresql://localhost/example")// Credentials.jdbcCredentials(user,password)// Automatic schema export.schemaToolingAction(Action.SPEC_ACTION_DROP_AND_CREATE)// SQL statement logging.showSql(true,true,true)// Create a new SessionFactory.createEntityManagerFactory();

Alternatively, the venerable classConfiguration offers similar functionality.

Advanced configuration options

Actually, these APIs are very simple facades resting on the much more powerful—​but also more complex—​APIs defined in the packageorg.hibernate.boot.This API is useful if you have very advanced requirements, for example, if you’re writing a framework or implementing a container.You’ll find more information in theUser Guide, and in thepackage-level documentation oforg.hibernate.boot.

2.5. Configuration using Hibernate properties file

If we’re using programmatic configuration, but we don’t want to put certain configuration properties directly in the Java code, we can specify them in a file namedhibernate.properties, and place the file in the root classpath.

# PostgreSQLjakarta.persistence.jdbc.url=jdbc:postgresql://localhost/example# Credentialsjakarta.persistence.jdbc.user=hibernatejakarta.persistence.jdbc.password=zAh7mY$2MNshzAQ5# SQL statement logginghibernate.show_sql=truehibernate.format_sql=truehibernate.highlight_sql=true

2.6. Basic configuration settings

ThePersistenceConfiguration class declaresstatic final constants holding the names of all configuration properties defined by the specification itself, for example,JDBC_URL holds the property name"jakarta.persistence.jdbc.driver".

Similarly, the classAvailableSettings enumerates all the configuration properties understood by Hibernate.

Of course, we’re not going to cover every useful configuration setting in this chapter.Instead, we’ll mention the ones you need to get started, and come back to some other important settings later, especially when we talk about performance tuning.

Hibernate has many—too many—switches and toggles.Please don’t go crazy messing about with these settings; most of them are rarely needed, and many only exist to provide backward compatibility with older versions of Hibernate.With rare exception, the default behavior of every one of these settings was carefully chosen to bethe behavior we recommend.

The properties you really do need to get started are these three:

Table 4. JDBC connection settings
Configuration property namePurpose

jakarta.persistence.jdbc.url

JDBC URL of your database

jakarta.persistence.jdbc.user andjakarta.persistence.jdbc.password

Your database credentials

Since Hibernate 6, you don’t need to specifyhibernate.dialect.The correct Hibernate SQLDialect will be determined for you automatically.The only reason to specify this property is if you’re using a custom user-writtenDialect class.

Similarly, neitherhibernate.connection.driver_class norjakarta.persistence.jdbc.driver is needed when working with one of the supported databases.

In some environments it’s useful to be able to start Hibernate without accessing the database.In this case, we must explicitly specify not only the database platform, but also the version of the database, using the standard JPA configuration properties.

# disable use of JDBC database metadatahibernate.boot.allow_jdbc_metadata_access=false# explicitly specify database and versionjakarta.persistence.database-product-name=PostgreSQLjakarta.persistence.database-major-version=15jakarta.persistence.database-minor-version=7

The product name is the value returned byjava.sql.DatabaseMetaData.getDatabaseProductName(), for example,PostgreSQL,MySQL,H2,Oracle,EnterpriseDB,MariaDB, orMicrosoft SQL Server.

Table 5. Settings needed when database is inaccessible at startup
Configuration property namePurpose

hibernate.boot.allow_jdbc_metadata_access

Set tofalse to disallow access to the database at startup

jakarta.persistence.database-product-name

The database product name, according to the JDBC driver

jakarta.persistence.database-major-version andjakarta.persistence.database-minor-version

The major and minor versions of the database

Pooling JDBC connections is an extremely important performance optimization.You can set the size of Hibernate’s built-in connection pool using this property:

Table 6. Built-in connection pool size
Configuration property namePurpose

hibernate.connection.pool_size

The size of the connection pool

This configuration property is also respected when you use Agroal, HikariCP, or c3p0 for connection pooling.

By default, Hibernate uses a simplistic built-in connection pool.This pool is not meant for use in production, and later, when we discuss performance, we’ll see how toselect a more robust implementation.

Alternatively, in a container environment, you’ll need at least one of these properties:

Table 7. Transaction management settings
Configuration property namePurpose

jakarta.persistence.transactionType

(Optional, defaults toJTA) Determines if transaction management is via JTA or resource-local transactions. SpecifyRESOURCE_LOCAL if JTA should not be used.

jakarta.persistence.jtaDataSource

JNDI name of a JTA datasource

jakarta.persistence.nonJtaDataSource

JNDI name of a non-JTA datasource

In this case, Hibernate obtains pooled JDBC database connections from a container-managedDataSource.

2.7. Automatic schema export

You can have Hibernate infer your database schema from the mappingannotations you’ve specified in your Java code, and export the schema atinitialization time by specifying one or more of the following configurationproperties:

Table 8. Schema management settings
Configuration property namePurpose

jakarta.persistence.schema-generation.database.action

  • Ifdrop-and-create, first drop the schema, then export tables, sequences, and constraints, and then populate initial data

  • Ifcreate, export tables, sequences, and constraints, without attempting to drop them first, and then populate initial data

  • Ifcreate-drop, drop the schema and recreate it onSessionFactory startup;additionally, drop the schema onSessionFactory shutdown

  • Ifdrop, drop the schema onSessionFactory shutdown

  • Ifvalidate, validate the database schema without changing it

  • Ifupdate, only export what’s missing in the schema, and alter incorrect column types

  • Ifpopulate, only populate initial data

jakarta.persistence.create-database-schemas

(Optional) Iftrue, automatically create schemas and catalogs

jakarta.persistence.schema-generation.create-source

(Optional) Ifmetadata-then-script orscript-then-metadata, execute an additional SQL script when exported tables and sequences

jakarta.persistence.schema-generation.create-script-source

(Optional) The name of a SQL DDL script to be executed

jakarta.persistence.sql-load-script-source

(Optional) The name of a SQL DML script to be executed

This feature is extremely useful for testing.

The easiest way to pre-initialize a database with test or "reference" data is to place a list of SQLinsert statements in a file named, for example,import.sql, and specify the path to this file using the propertyjakarta.persistence.sql-load-script-source.We’ve already seen anexample of this approach, which is cleaner than writing Java code to instantiate entity instances and callingpersist() on each of them.

As we mentionedearlier, it can also be useful to control schema export programmatically.

TheSchemaManager API allows programmatic control over schema export:

sessionFactory.getSchemaManager().create(true);

2.8. Logging the generated SQL

To see the generated SQL as it’s sent to the database, you have two options.

One way is to set the propertyhibernate.show_sql totrue, and Hibernate will log SQL directly to the console.You can make the output much more readable by enabling formatting or highlighting.These settings really help when troubleshooting the generated SQL statements.

Table 9. Settings for SQL logging to the console
Configuration property namePurpose

hibernate.show_sql

Iftrue, log SQL directly to the console

hibernate.format_sql

Iftrue, log SQL in a multiline, indented format

hibernate.highlight_sql

Iftrue, log SQL with syntax highlighting via ANSI escape codes

Alternatively, you can enable debug-level logging for the categoryorg.hibernate.SQL using your preferred SLF4J logging implementation.

For example, if you’re using Log4J 2 (as above inOptional dependencies), add these lines to yourlog4j2.properties file:

# SQL executionlogger.hibernate.name=org.hibernate.SQLlogger.hibernate.level=debug# JDBC parameter bindinglogger.jdbc-bind.name=org.hibernate.orm.jdbc.bindlogger.jdbc-bind.level=trace# JDBC result set extractionlogger.jdbc-extract.name=org.hibernate.orm.jdbc.extractlogger.jdbc-extract.level=trace

SQL logging respects the settingshibernate.format_sql andhibernate.highlight_sql, so we don’t miss out on the pretty formatting and highlighting.

2.9. Minimizing repetitive mapping information

The following properties are very useful for minimizing the amount of information you’ll need to explicitly specify in@Table and@Column annotations, which we’ll discuss below inObject/relational mapping:

Table 10. Settings for minimizing explicit mapping information
Configuration property namePurpose

hibernate.default_schema

A default schema name for entities which do not explicitly declare one

hibernate.default_catalog

A default catalog name for entities which do not explicitly declare one

hibernate.physical_naming_strategy

APhysicalNamingStrategy implementing your database naming standards

hibernate.implicit_naming_strategy

AnImplicitNamingStrategy which specifies how "logical" names of relational objects should be inferred when no name is specified in annotations

Writing your ownPhysicalNamingStrategy and/orImplicitNamingStrategy is an especially good way to reduce the clutter of annotations on your entity classes, and to implement your database naming conventions, and so we think you should do it for any nontrivial data model.We’ll have more to say about them inNaming strategies.

2.10. Quoting SQL identifiers

By default, Hibernate never quotes a SQL table or column name in generated SQL when the name contains only alphanumeric characters.This behavior is usually much more convenient, especially when working with a legacy schema, since unquoted identifiers aren’t case-sensitive, and so Hibernate doesn’t need to know or care whether a column is namedNAME,name, orName on the database side.On the other hand, any table or column name containing a punctuation character like$ is automatically quoted by default.

The following settings enable additional automatic quoting:

Table 11. Settings for identifier quoting
Configuration property namePurpose

hibernate.auto_quote_keyword

Automatically quote any identifier which is a SQL keyword

hibernate.globally_quoted_identifiers

Automatically quote every identifier

Note thathibernate.globally_quoted_identifiers is a synonym for<delimited-identifiers/> inpersistence.xml.We don’t recommend the use of global identifier quoting, and in fact these settings are rarely used.

A better alternative is to explicitly quote table and column names where necessary, by writing@Table(name="\"View\") or@Column(name="\"number\"").Since that’s kinda ugly, Hibernate lets us use a backtick as the quote character instead of the double quote.

2.11. Nationalized character data in SQL Server

By default, SQL Server’schar andvarchar types don’t accommodate Unicode data.But a Java string may contain any Unicode character.So, if you’re working with SQL Server, you might need to force Hibernate to use thenchar andnvarchar column types.

Table 12. Setting the use of nationalized character data
Configuration property namePurpose

hibernate.use_nationalized_character_data

Usenchar andnvarchar instead ofchar andvarchar

On the other hand, if onlysome columns store nationalized data, use the@Nationalized annotation to indicate fields of your entities which map these columns.

Alternatively, you can configure SQL Server to use the UTF-8 enabled collation_UTF8.

2.12. Date and time types and JDBC

By default, Hibernate handles date and time types defined byjava.time by:

  • convertingjava.time types to JDBC date/time types defined injava.sql when sending data to the database, and

  • readingjava.sql types from JDBC and then converting them tojava.time types when retrieving data from the database.

This works best when the database server time zone agrees with JVM system time zone.

We therefore recommend setting things up so that the database server and the JVM agree on the same time zone.Hint: when in doubt, UTC is quite a nice time zone.

There are two system configuration properties which influence this behavior:

Table 13. Settings for JDBC date/time handling
Configuration property namePurpose

hibernate.jdbc.time_zone

Use an explicit time zone when interacting with JDBC

hibernate.type.java_time_use_direct_jdbc

Read and writejava.time types directly to and from JDBC

You may sethibernate.jdbc.time_zone to the time zone of the database server if for some reason the JVM needs to operate in a different time zone.We do not recommend this approach.

On the other hand, we would love to recommend the use ofhibernate.type.java_time_use_direct_jdbc, but this option is still experimental for now, and does result in some subtle differences in behavior which might affect legacy programs using Hibernate.

3. Entities

Anentity is a Java class which represents data in a relational database table.We say that the entitymaps ormaps to the table.Much less commonly, an entity might aggregate data from multiple tables, but we’ll get to thatlater.

An entity hasattributes—properties or fields—which map to columns of the table.In particular, every entity must have anidentifier orid, which maps to the primary key of the table.The id allows us to uniquely associate a row of the table with an instance of the Java class, at least within a givenpersistence context.

We’ll explore the idea of a persistence contextlater. For now, think of it as a one-to-one mapping between ids and entity instances.

An instance of a Java class cannot outlive the virtual machine to which it belongs.But we may think of an entity instance having a lifecycle which transcends a particular instantiation in memory.By providing its id to Hibernate, we may re-materialize the instance in a new persistence context, as long as the associated row is present in the database.Therefore, the operationspersist() andremove() may be thought of as demarcating the beginning and end of the lifecycle of an entity, at least with respect to persistence.

Thus, an id represents thepersistent identity of an entity, an identity that outlives a particular instantiation in memory.And this is an important difference between entity class itself and the values of its attributes—the entity has a persistent identity, and a well-defined lifecycle with respect to persistence, whereas aString orList representing one of its attribute values doesn’t.

An entity usually has associations to other entities.Typically, an association between two entities maps to a foreign key in one of the database tables.A group of mutually associated entities is often called adomain model, thoughdata model is also a perfectly good term.

3.1. Entity classes

An entity must:

  • be a non-final class,

  • with a non-private constructor with no parameters.

On the other hand, the entity class may be either concrete orabstract, and it may have any number of additional constructors.

An entity class may be astatic inner class.

Every entity class must be annotated@Entity.

@EntityclassBook{Book(){}...}

Alternatively, the class may be identified as an entity type by providing an XML-based mapping for the class.

Mapping entities using XML

When XML-based mappings are used, the<entity> element is used to declare an entity class:

<entity-mappings><package>org.hibernate.example</package><entityclass="Book"><attributes> ...</attributes></entity>    ...</entity-mappings>

Since theorm.xml mapping file format defined by the JPA specification was modelled closely on the annotation-based mappings, it’s usually easy to go back and forth between the two options.

We won’t have much more to say about XML-based mappings in this Short Guide, since it’s not our preferred way to do things.

"Dynamic" models

We love representing entities as classes because the classes give us atype-safe model of our data.But Hibernate also has the ability to represent entities as detyped instances ofjava.util.Map.There’s information in theUser Guide, if you’re curious.

This must sound like a weird feature for a project that places importance on type-safety.Actually, it’s a useful capability for a very particular sort of generic code.For example,Hibernate Envers is a great auditing/versioning system for Hibernate entities.Envers makes use of maps to represent itsversioned model of the data.

3.2. Access types

Each entity class has a defaultaccess type, either:

  • directfield access, or

  • property access.

Hibernate automatically determines the access type from the location of attribute-level annotations.Concretely:

  • if a field is annotated@Id, field access is used, or

  • if a getter method is annotated@Id, property access is used.

Back when Hibernate was just a baby, property access was quite popular in the Hibernate community.Today, however, field access ismuch more common.

The default access type may be specified explicitly using the@Access annotation, but we strongly discourage this, since it’s ugly and never necessary.

Mapping annotations should be placed consistently:

  • if@Id annotates a field, the other mapping annotations should also be applied to fields, or,

  • if@Id annotates a getter, the other mapping annotations should be applied to getters.

It is in principle possible to mix field and property access using explicit@Access annotations at the attribute level.We don’t recommend doing this.

An entity class likeBook, which does not extend any other entity class, is called aroot entity.Every root entity must declare an identifier attribute.

3.3. Entity class inheritance

An entity class mayextend another entity class.

@EntityclassAudioBookextendsBook{AudioBook(){}...}

A subclass entity inherits every persistent attribute of every entity it extends.

A root entity may also extend another class and inherit mapped attributes from the other class.But in this case, the class which declares the mapped attributes must be annotated@MappedSuperclass.

@MappedSuperclassclassVersioned{...}@EntityclassBookextendsVersioned{...}

A root entity class must declare an attribute annotated@Id, or inherit one from a@MappedSuperclass.A subclass entity always inherits the identifier attribute of the root entity.It may not declare its own@Id attribute.

3.4. Identifier attributes

An identifier attribute is usually a field:

@EntityclassBook{Book(){}@IdLongid;...}

But it may be a property:

@EntityclassBook{Book(){}privateLongid;@IdLonggetId(){returnid;}voidsetId(Longid){this.id=id;}...}

An identifier attribute must be annotated@Id or@EmbeddedId.

Identifier values may be:

  • assigned by the application, that is, by your Java code, or

  • generated and assigned by Hibernate.

We’ll discuss the second option first.

3.5. Generated identifiers

An identifier is often system-generated, in which case it should be annotated@GeneratedValue:

@Id@GeneratedValueLongid;

System-generated identifiers, orsurrogate keys make it easier to evolve or refactor the relational data model.If you have the freedom to define the relational schema, we recommend the use of surrogate keys.On the other hand, if, as is more common, you’re working with a pre-existing database schema, you might not have the option.

JPA defines the following strategies for generating ids, which are enumerated byGenerationType:

Table 14. Standard id generation strategies
StrategyJava typeImplementation

GenerationType.UUID

UUID orString

A JavaUUID

GenerationType.IDENTITY

Long orInteger

An identity or autoincrement column

GenerationType.SEQUENCE

Long orInteger

A database sequence

GenerationType.TABLE

Long orInteger

A database table

GenerationType.AUTO

Long orInteger

SelectsSEQUENCE,TABLE, orUUID based on the identifier type and capabilities of the database

For example, this UUID is generated in Java code:

@Id@GeneratedValueUUIDid;// AUTO strategy selects UUID based on the field type

This id maps to a SQLidentity,auto_increment, orbigserial column:

@Id@GeneratedValue(strategy=IDENTITY)Longid;

The@SequenceGenerator and@TableGenerator annotations allow further control overSEQUENCE andTABLE generation respectively.

Consider this sequence generator:

@SequenceGenerator(name="bookSeq",sequenceName="seq_book",initialValue=5,allocationSize=10)

Values are generated using a database sequence defined as follows:

createsequenceseq_bookstartwith5incrementby10

Notice that Hibernate doesn’t have to go to the database every time a new identifier is needed.Instead, a given process obtains a block of ids, of sizeallocationSize, and only needs to hit the database each time the block is exhausted.Of course, the downside is that generated identifiers are not contiguous.

If you let Hibernate export your database schema, the sequence definition will have the rightstart with andincrement values.But if you’re working with a database schema managed outside Hibernate, make sure theinitialValue andallocationSize members of@SequenceGenerator match thestart with andincrement specified in the DDL.

Any identifier attribute may now make use of the generator namedbookSeq:

@Id@GeneratedValue(generator="bookSeq")// reference to generator defined elsewhereLongid;

Actually, it’s extremely common to place the@SequenceGenerator annotation on the@Id attribute that makes use of it:

@Id@GeneratedValue// uses the generator defined below@SequenceGenerator(sequenceName="seq_book",initialValue=5,allocationSize=10)Longid;

In this case, thename of the@SequenceGenerator should not be specified.

We may even place a@SequenceGenerator or@TableGenerator annotation at the package level:

@SequenceGenerator(sequenceName="id_sequence",initialValue=5,allocationSize=10)@TableGenerator(table="id_table",initialValue=5,allocationSize=10)packageorg.example.entities;

Then any entity in this package which specifiesstrategy=SEQUENCE orstrategy=TABLE without also explicitly specifying a generatorname will be assigned a generator based on the package-level annotation.

@Id@GeneratedValue(strategy=SEQUENCE)// uses the sequence generator defined at the package levelLongid;

As you can see, JPA provides quite adequate support for the most common strategies for system-generated ids.However, the annotations themselves are a bit more intrusive than they should be, and there’s no well-defined way to extend this framework to support custom strategies for id generation.Nor may@GeneratedValue be used on a property not annotated@Id.Since custom id generation is a rather common requirement, Hibernate provides a very carefully-designed framework for user-definedGenerators, which we’ll discuss inUser-defined generators.

3.6. Natural keys as identifiers

Not every identifier attribute maps to a (system-generated) surrogate key.Primary keys which are meaningful to the user of the system are callednatural keys.

When the primary key of a table is a natural key, we don’t annotate the identifier attribute@GeneratedValue, and it’s the responsibility of the application code to assign a value to the identifier attribute.

@EntityclassBook{@IdStringisbn;...}

Of particular interest are natural keys which comprise more than one database column, and such natural keys are calledcomposite keys.

3.7. Composite identifiers

If your database uses composite keys, you’ll need more than one identifier attribute.There are two ways to map composite keys in JPA:

  • using an@IdClass, or

  • using an@EmbeddedId.

Perhaps the most immediately-natural way to represent this in an entity class is with multiple fields annotated@Id, for example:

@Entity@IdClass(BookId.class)classBook{Book(){}@IdStringisbn;@Idintprinting;...}

But this approach comes with a problem: what object can we use to identify aBook and pass to methods likefind() which accept an identifier?

The solution is to write a separate class with fields that match the identifier attributes of the entity.Every such id class must overrideequals() andhashCode().Of course, the easiest way to satisfy these requirements is to declare the id class as arecord.

recordBookId(Stringisbn,intprinting){}

The@IdClass annotation of theBook entity identifiesBookId as the id class to use for that entity.

This is not our preferred approach.Instead, we recommend that theBookId class be declared as an@Embeddable type:

@EmbeddablerecordBookId(Stringisbn,intprinting){}

We’ll learn more aboutEmbeddable objects below.

Now the entity class may reuse this definition using@EmbeddedId, and the@IdClass annotation is no longer required:

@EntityclassBook{Book(){}@EmbeddedIdBookIdbookId;...}

This second approach eliminates some duplicated code.

Either way, we may now useBookId to obtain instances ofBook:

Bookbook=session.find(Book.class,newBookId(isbn,printing));

3.8. Version attributes

An entity may have an attribute which is used by Hibernate foroptimistic lock verification.Aversion attribute is usually of typeInteger,Short,Long,LocalDateTime,OffsetDateTime,ZonedDateTime, orInstant.

@Versionintversion;
@VersionLocalDateTimelastUpdated;

A version attribute is automatically assigned by Hibernate when an entity is made persistent, and automatically incremented or updated each time the entity is updated.

If the version attribute is numeric, then an entity is, by default, assigned the version number0 when it’s first made persistent.It’s easy to specify that the initial version should be assigned the number1 instead:

@Versionintversion=1;// the initial version number

Almost every entity which is frequently updated should have a version attribute.

If an entity doesn’t have a version number, which often happens when mapping legacy data, we can still do optimistic locking.The@OptimisticLocking annotation lets us specify that optimistic locks should be checked by validating the values ofALL fields, or only theDIRTY fields of the entity.And the@OptimisticLock annotation lets us selectively exclude certain fields from optimistic locking.

The@Id and@Version attributes we’ve already seen are just specialized examples ofbasic attributes.

3.9. Natural id attributes

Even when an entity has a surrogate key, it should always be possible to write down a combination of fields which uniquely identifies an instance of the entity, from the point of view of the user of the system.This combination of fields is its natural key.Above, weconsidered the case where the natural key coincides with the primary key.Here, the natural key is a second unique key of the entity, distinct from its surrogate primary key.

If you can’t identify a natural key, it might be a sign that you need to think more carefully about some aspect of your data model.If an entity doesn’t have a meaningful unique key, then it’s impossible to say what event or object it represents in the "real world" outside your program.

Since it’sextremely common to retrieve an entity based on its natural key, Hibernate has a way to mark the attributes of the entity which make up its natural key.Each attribute must be annotated@NaturalId.

@EntityclassBook{Book(){}@Id@GeneratedValueLongid;// the system-generated surrogate key@NaturalIdStringisbn;// belongs to the natural key@NaturalIdintprinting;// also belongs to the natural key...}

Hibernate automatically generates aUNIQUE constraint on the columns mapped by the annotated fields.

Consider using the natural id attributes to implementequals() andhashCode().

The payoff for doing this extra work, as we will seemuch later, is that we can take advantage of optimized natural id lookups that make use of the second-level cache.

Note that even when you’ve identified a natural key, we still recommend the use of a generated surrogate key in foreign keys, since this makes your data modelmuch easier to change.

3.10. Basic attributes

Abasic attribute of an entity is a field or property which maps to a single column of the associated database table.The JPA specification defines a quite limited set of basic types:

Table 15. JPA-standard basic attribute types
ClassificationPackageTypes

Primitive types

boolean,int,double, etc

Primitive wrappers

java.lang

Boolean,Integer,Double, etc

Strings

java.lang

String

Arbitrary-precision numeric types

java.math

BigInteger,BigDecimal

UUIDs

java.util

UUID

Date/time types

java.time

LocalDate,LocalTime,LocalDateTime,OffsetDateTime,Instant,Year

Deprecated date/time types 💀

java.util

Date,Calendar

Deprecated JDBC date/time types 💀

java.sql

Date,Time,Timestamp

Binary and character arrays

byte[],char[]

Binary and character wrapper arrays 💀

java.lang

Byte[],Character[]

Enumerated types

Anyenum

Serializable types

Any type which implementsjava.io.Serializable

We’re begging you to use types from thejava.time package instead of anything which inheritsjava.util.Date.

The use ofByte[] andCharacter[] as basic types was deprecated by Jakarta Persistence 3.2.Hibernate does not allownull elements in such arrays.Usebyte[] orchar[] instead.

Serializing a Java object and storing its binary representation in the database is usually wrong.As we’ll soon see inEmbeddable objects, Hibernate has much better ways to handle complex Java objects.

Hibernate slightly extends this list with the following types:

Table 16. Additional basic attribute types in Hibernate
ClassificationPackageTypes

Additional date/time types

java.time

Duration,ZoneId,ZoneOffset, and evenZonedDateTime

JDBC LOB types

java.sql

Blob,Clob,NClob

Java class object

java.lang

Class

Miscellaneous types

java.util

Currency,Locale,URL,TimeZone

The@Basic annotation explicitly specifies that an attribute is basic, but it’s often not needed, since attributes are assumed basic by default.On the other hand, if a non-primitively-typed attribute cannot be null, use of@Basic(optional=false) is highly recommended.

@Basic(optional=false)StringfirstName;@Basic(optional=false)StringlastName;StringmiddleName;// may be null

Note that primitively-typed attributes are inferredNOT NULL by default.

How to make a columnnot null in JPA

There are two standard ways to add aNOT NULL constraint to a mapped column in JPA:

  • using@Basic(optional=false), or

  • using@Column(nullable=false).

You might wonder what the difference is.

Well, it’s perhaps not obvious to a casual user of the JPA annotations, but they actually come in two "layers":

  • annotations like@Entity,@Id, and@Basic belong to thelogical layer, the subject of the current chapter—they specify the semantics of your Java domain model, whereas

  • annotations like@Table and@Column belong to themapping layer, the topic of thenext chapter—they specify how elements of the domain model map to objects in the relational database.

Information may be inferred from the logical layer down to the mapping layer, but is never inferred in the opposite direction.

Now, the@Column annotation, to whom we’ll be properlyintroduced a bit later, belongs to themapping layer, and so itsnullable member only affects schema generation (resulting in anot null constraint in the generated DDL).On the other hand, the@Basic annotation belongs to the logical layer, and so an attribute markedoptional=false is checked by Hibernate before it even writes an entity to the database.Note that:

  • optional=false impliesnullable=false, but

  • nullable=falsedoes not implyoptional=false.

Therefore, we prefer@Basic(optional=false) to@Column(nullable=false).

But wait!An even better solution is to use the@NotNull annotation from Bean Validation.Just add Hibernate Validator to your project build, as described inOptional dependencies.

3.11. Enumerated types

We included Javaenums on the list above.An enumerated type is considered a sort of basic type, but since most databases don’t have a nativeENUM type, JPA provides a special@Enumerated annotation to specify how the enumerated values should be represented in the database:

  • by default, an enum is stored as an integer, the value of itsordinal() member, but

  • if the attribute is annotated@Enumerated(STRING), it will be stored as a string, the value of itsname() member.

//here, an ORDINAL encoding makes sense@Enumerated@Basic(optional=false)DayOfWeekdayOfWeek;//but usually, a STRING encoding is better@Enumerated(EnumType.STRING)@Basic(optional=false)Statusstatus;

The@EnumeratedValue annotation allows the column value to be customized:

enumResolution{UNRESOLVED(0),FIXED(1),REJECTED(-1);@EnumeratedValue// store the code, not the enum ordinal() valuefinalintcode;Resolution(intcode){this.code=code;}}

Since Hibernate 6, anenum annotated@Enumerated(STRING) is mapped to:

  • aVARCHAR column type with aCHECK constraint on most databases, or

  • anENUM column type on MySQL.

Any otherenum is mapped to aTINYINT column with aCHECK constraint.

JPA picks the wrong default here.In most cases, storing an integer encoding of theenum value makes the relational data harder to interpret.

Even consideringDayOfWeek, the encoding to integers is ambiguous.If you checkjava.time.DayOfWeek, you’ll notice thatSUNDAY is encoded as6.But in the country I was born,SUNDAY is thefirst day of the week!

So we prefer@Enumerated(STRING) for mostenum attributes.

An interesting special case arises on PostgreSQL and Oracle.

Named enumerated types

Some databases supportnamedENUM types, which must be declared using in DDL using:

  • CREATE TYPE …​ AS ENUM on PostgreSQL, or

  • CREATE DOMAIN …​ AS ENUM on Oracle.

These look like a perfect match for Javaenums, which also have names!

Sadly, theseENUM types aren’t well-integrated with the SQL language, nor well-supported by the JDBC drivers, so Hibernate doesn’t use them by default.But if you would like to use a named enumerated type on Postgres or Oracle, just annotate yourenum attribute like this:

@JdbcTypeCode(SqlTypes.NAMED_ENUM)@Basic(optional=false)Statusstatus;

Alternatively, you may enable the configuration propertyhibernate.type.prefer_native_enum_types.

The limited set of pre-defined basic attribute types can be stretched a bit further by supplying aconverter.

3.12. Converters

A JPAAttributeConverter is responsible for:

  • converting a given Java type to one of the types listed above, and/or

  • perform any other sort of pre- and post-processing you might need to perform on a basic attribute value before writing and reading it to or from the database.

Converters substantially widen the set of attribute types that can be handled by JPA.

There are two ways to apply a converter:

  • the@Convert annotation applies anAttributeConverter to a particular entity attribute, or

  • the@Converter annotation (or, alternatively, the@ConverterRegistration annotation) registers anAttributeConverter for automatic application to all attributes of a given type.

For example, the following converter will be automatically applied to any attribute of typeEnumSet<DayOfWeek>, and takes care of persisting theEnumSet<DayOfWeek> to a column of typeINTEGER:

@Converter(autoApply=true)publicstaticclassEnumSetConverter// converts Java values of type EnumSet<DayOfWeek> to integers for storage in an INT columnimplementsAttributeConverter<EnumSet<DayOfWeek>,Integer>{@OverridepublicIntegerconvertToDatabaseColumn(EnumSet<DayOfWeek>enumSet){intencoded=0;varvalues=DayOfWeek.values();for(inti=0;i<values.length;i++){if(enumSet.contains(values[i])){encoded|=1<<i;}}returnencoded;}@OverridepublicEnumSet<DayOfWeek>convertToEntityAttribute(Integerencoded){varset=EnumSet.noneOf(DayOfWeek.class);varvalues=DayOfWeek.values();for(inti=0;i<values.length;i++){if(((1<<i)&encoded)!=0){set.add(values[i]);}}returnset;}}

On the other hand, if wedon’t setautoapply=true, then we must explicitly apply the converter using the@Convert annotation:

@Convert(converter=EnumSetConverter.class)@Basic(optional=false)EnumSet<DayOfWeek>daysOfWeek;

All this is nice, but it probably won’t surprise you that Hibernate goes beyond what is required by JPA.

3.13. Compositional basic types

Hibernate considers a "basic type" to be formed by the marriage of two objects:

  • aJavaType, which models the semantics of a certain Java class, and

  • aJdbcType, representing a SQL type which is understood by JDBC.

When mapping a basic attribute, we may explicitly specify aJavaType, aJdbcType, or both.

JavaType

An instance oforg.hibernate.type.descriptor.java.JavaType represents a particular Java class.It’s able to:

  • compare instances of the class to determine if an attribute of that class type is dirty (modified),

  • produce a useful hash code for an instance of the class,

  • coerce values to other types, and, in particular,

  • convert an instance of the class to one of several other equivalent Java representations at the request of its partnerJdbcType.

For example,IntegerJavaType knows how to convert anInteger orint value to the typesLong,BigInteger, andString, among others.

We may explicitly specify a Java type using the@JavaType annotation, but for the built-inJavaTypes this is never necessary.

@JavaType(LongJavaType.class)// not needed, this is the default JavaType for longlongcurrentTimeMillis;

For a user-writtenJavaType, the annotation is more useful:

@JavaType(BitSetJavaType.class)BitSetbitSet;

Alternatively, the@JavaTypeRegistration annotation may be used to registerBitSetJavaType as the defaultJavaType forBitSet.

JdbcType

Anorg.hibernate.type.descriptor.jdbc.JdbcType is able to read and write a single Java type from and to JDBC.

For example,VarcharJdbcType takes care of:

  • writing Java strings to JDBCPreparedStatements by callingsetString(), and

  • reading Java strings from JDBCResultSets usinggetString().

By pairingLongJavaType withVarcharJdbcType in holy matrimony, we produce a basic type which mapsLongs and primitivelongss to the SQL typeVARCHAR.

We may explicitly specify a JDBC type using the@JdbcType annotation.

@JdbcType(VarcharJdbcType.class)longcurrentTimeMillis;

Alternatively, we may specify a JDBC type code:

@JdbcTypeCode(Types.VARCHAR)longcurrentTimeMillis;

The@JdbcTypeRegistration annotation may be used to register a user-writtenJdbcType as the default for a given SQL type code.

JDBC types and JDBC type codes

The types defined by the JDBC specification are enumerated by the integer type codes in the classjava.sql.Types.Each JDBC type is an abstraction of a commonly-available type in SQL.For example,Types.VARCHAR represents the SQL typeVARCHAR (orVARCHAR2 on Oracle).

Since Hibernate understands more SQL types than JDBC, there’s an extended list of integer type codes in the classorg.hibernate.type.SqlTypes.For example,SqlTypes.GEOMETRY represents the spatial data typeGEOMETRY.

AttributeConverter

If a givenJavaType doesn’t know how to convert its instances to the type required by its partnerJdbcType, we must help it out by providing a JPAAttributeConverter to perform the conversion.

For example, to form a basic type usingLongJavaType andTimestampJdbcType, we would provide anAttributeConverter<Long,Timestamp>.

@JdbcType(TimestampJdbcType.class)@Convert(converter=LongToTimestampConverter.class)longcurrentTimeMillis;

Let’s abandon our analogy right here, before we start calling this basic type a "throuple".

3.14. Date and time types, and time zones

Dates and times should always be represented using the types defined injava.time.

Never use the legacy typesjava.sql.Date,java.sql.Time,java.sql.Timestamp, orjava.util.Date.At our urging, support for these types has even beenofficially deprecated in JPA 3.2.Eventually, we hope to completely remove support for these types from the JPA spec and from Hibernate.

Some of the types injava.time map naturally to an ANSI SQL column type.A source of confusion is that some databases still don’t follow the ANSI standard naming here.Also, as you’re probably aware, theDATE type on Oracle is not an ANSI SQLDATE.In fact, Oracle doesn’t haveDATE orTIME types—​every date or time must be stored as a timestamp.

Table 17. Type mappings fromjava.time to ANSI SQL
java.time classANSI SQL typeMySQLSQL ServerOracle

LocalDate

DATE

DATE

DATE

DATE 💀

LocalTime

TIME

TIME

TIME

TIMESTAMP 💀

LocalDateTime

TIMSTAMP

DATETIME

DATETIME2

TIMESTAMP

OffsetDateTime,ZonedDateTime

TIMESTAMP WITH TIME ZONE

TIMESTAMP 🙄

DATETIMEOFFSET

TIMESTAMP WITH TIME ZONE

On the other hand, there are no perfectly natural mappings forInstant andDuration on must databases.By default:

  • Duration is mapped to a column of typeNUMERIC(21) holding the length of the duration in nanoseconds, and

  • Instant is mapped to a column of typeTIMESTAMP (DATETIME on MySQL).

Fortunately, these mappings can be modified by specifying theJdbcType.

For example, if we wanted to store anInstant usingTIMESTAMP WITH TIME ZONE (TIMESTAMP on MySQL) instead ofTIMESTAMP, then we could annotate the field:

// store the Instant as a TIMESTAMP WITH TIME ZONE, instead of as a TIMESTAMP@JdbcTypeCode(SqlTypes.TIMESTAMP_WITH_TIMEZONE)Instantinstant;

Alternatively, we could set the configuration propertyhibernate.type.preferred_instant_jdbc_type:

// store field of type Instant as TIMESTAMP WITH TIME ZONE, instead of as a TIMESTAMPconfig.setProperty(MappingSettings.PREFERRED_INSTANT_JDBC_TYPE,SqlTypes.TIMESTAMP_WITH_TIMEZONE);

We have worked very hard to make sure that Java date and time types work with consistent and correct semantics across all databases supported by Hibernate.In particular, Hibernate is very careful in how it handles time zones.

Unfortunately, with the notable exception of Oracle, most SQL databases feature embarrassingly poor support for timezones.Even some databases which do supposedly supportTIMESTAMP WITH TIME ZONE simply covert the datetime to UTC.Here, Hibernate is limited by the capabilities of the databases themselves, and so on many databases, time zone information will not, by default, be preserved for anOffsetDateTime orZonedDateTime.

The still-experimental annotation@TimeZoneStorage provides some additional options in case the default behavior falls short.

3.15. Embeddable objects

An embeddable object is a Java class whose state maps to multiple columns of a table, but which doesn’t have its own persistent identity.That is, it’s a class with mapped attributes, but no@Id attribute.

An embeddable object can only be made persistent by assigning it to the attribute of an entity.Since the embeddable object does not have its own persistent identity, its lifecycle with respect to persistence is completely determined by the lifecycle of the entity to which it belongs.

An embeddable class must be annotated@Embeddable instead of@Entity.

@EmbeddableclassName{@Basic(optional=false)StringfirstName;@Basic(optional=false)StringlastName;StringmiddleName;Name(){}Name(StringfirstName,StringmiddleName,StringlastName){this.firstName=firstName;this.middleName=middleName;this.lastName=lastName;}...}

An embeddable class must satisfy the same requirements that entity classes satisfy, with the exception that an embeddable class has no@Id attribute.In particular, it must have a constructor with no parameters.

Alternatively, an embeddable type may be defined as a Java record type:

@EmbeddablerecordName(StringfirstName,StringmiddleName,StringlastName){}

In this case, the requirement for a constructor with no parameters is relaxed.

We may now use ourName class (or record) as the type of an entity attribute:

@EntityclassAuthor{@Id@GeneratedValueLongid;Namename;...}

Embeddable types can be nested.That is, an@Embeddable class may have an attribute whose type is itself a different@Embeddable class.

JPA provides an@Embedded annotation to identify an attribute of an entity that refers to an embeddable type.This annotation is completely optional, and so we don’t usually use it.

On the other hand a reference to an embeddable type isnever polymorphic.One@Embeddable classF may inherit a second@Embeddable classE, but an attribute of typeE will always refer to an instance of that concrete classE, never to an instance ofF.

Usually, embeddable types are stored in a "flattened" format.Their attributes map columns of the table of their parent entity.Later, inMapping embeddable types to UDTs or to JSON, we’ll see a couple of different options.

An attribute of embeddable type represents a relationship between a Java object with a persistent identity, and a Java object with no persistent identity.We can think of it as a whole/part relationship.The embeddable object belongs to the entity, and can’t be shared with other entity instances.And it exists for only as long as its parent entity exists.

Next we’ll discuss a different kind of relationship: a relationship between Java objects which each have their own distinct persistent identity and persistence lifecycle.

3.16. Associations

Anassociation is a relationship between entities.We usually classify associations based on theirmultiplicity.IfE andF are both entity classes, then:

  • aone-to-one association relates at most one unique instanceE with at most one unique instance ofF,

  • amany-to-one association relates zero or more instances ofE with a unique instance ofF, and

  • amany-to-many association relates zero or more instances ofE with zero or more instance ofF.

An association between entity classes may be either:

  • unidirectional, navigable fromE toF but not fromF toE, or

  • bidirectional, and navigable in either direction.

In this example data model, we can see the sorts of associations which are possible:

Example data model

An astute observer of the diagram above might notice that the relationship we’ve presented as a unidirectional one-to-one association could reasonably be represented in Java using subtyping.This is quite normal.A one-to-one association is the usual way we implement subtyping in a fully-normalized relational model.It’s related to theJOINEDinheritance mapping strategy.

There are three annotations for mapping associations:@ManyToOne,@OneToMany, and@ManyToMany.They share some common annotation members:

Table 18. Association-defining annotation members
MemberInterpretationDefault value

cascade

Persistence operations which shouldcascade to the associated entity; a list ofCascadeTypes

{}

fetch

Whether the association iseagerlyfetched or may beproxied

  • LAZY for@OneToMany and@ManyToMany

  • EAGER for@ManyToOne 💀💀💀

targetEntity

The associated entity class

Determined from the attribute type declaration

optional

For a@ManyToOne or@OneToOne association, whether the association can benull

true

mappedBy

For a bidirectional association, an attribute of the associated entity which maps the association

By default, the association is assumed unidirectional

We’ll explain the effect of these members as we consider the various types of association mapping.

It’s not a requirement to representevery foreign key relationship as an association at the Java level.It’s perfectly acceptable to replace a@ManyToOne mapping with a basic-typed attribute holding an identifier, if it’s inconvenient to think of this relationship as an association at the Java level.That said, it’s possible to take this idea way to far.

💀 Aggregates 💀

It’s come to our attention that a vocal group of people advocate that Java entity classes should be broken up into tiny disconnected islands they call "aggregates". An aggregate—​at least as a first approximation—​corresponds roughly to what we would usually call a parent/child relationship.Simple examples of aggregates might beOrder/Item, orProduct/Part.According to this way of thinking, there should be no associationsbetween aggregates.So theItem.product association should be replaced withproductId,Part.manufacturer should be replaced withmanufacturerId, and so on.(Of course, the word "aggregate" may also be employed in other senses, but this is the sense we’re discussing right now.)

In the example we’ve been using,Book would not be permitted to have a collection of entity typeAuthor, and should instead hold only the ids of the authors, or perhaps instances of someBookAuthor type which duplicates some state ofAuthor and is disconnected from the rest of the model.

Let’s stipulate that this might be a perfectly natural thing to do in certain contexts, for example, when accessing a document database.But one context where it doesn’t usually make sense is when accessing a relational database via Hibernate.The reason is that Hibernate offersrich functionality for optimizing access to associated data, including:

But all this functionality is lost if Hibernate doesn’t know it’s dealing with an association, inevitably making the application program much more vulnerable to problems withN+1 selects, just as soon as we encounter a business requirement which involves data from more than one aggregate.(Always keep in mind that business requirements change much faster than relational data models!)

To put it mildly: this is not how JPA was ever intended to be used.

It’s difficult to respond charitably to most of the arguments in favor of this approach, since most of them don’t rise above the level of hand-waving at boxes on drawn on whiteboards.An argument wecan respond to is the concern that transparent lazy fetching can lead to "accidental" fetching of an association and the potential for N+1 selects.This is a legit concern, and one we worry about too, but where it’s really a problem we have a much better solution: just use aStatelessSession, or a Jakarta Data repository, whereassociation fetching is always an explicit operation.Indeed,StatelessSession even guards against accidentalupdates, sinceupdate() is always an explicit operation.

Now that we know that associations are actually good and useful, let’s see how to model the various kinds of association we might find need to map to a relational data model.We begin with the most common association multiplicity.

3.17. Many-to-one

A many-to-one association is the most basic sort of association we can imagine.It maps completely naturally to a foreign key in the database.Almost all the associations in your domain model are going to be of this form.

Later, we’ll see how to map a many-to-one association to anassociation table.

The@ManyToOne annotation marks the "to one" side of the association, so a unidirectional many-to-one association looks like this:

classBook{@Id@GeneratedValueLongid;@ManyToOne(fetch=LAZY)Publisherpublisher;...}

Here, theBook table has a foreign key column holding the identifier of the associatedPublisher.

A very unfortunate misfeature of JPA is that@ManyToOne associations are fetched eagerly by default.This is almost never what we want.Almost all associations should be lazy.The only scenario in whichfetch=EAGER makes sense is if we think there’s always avery high probability that theassociated object will be found in the second-level cache.Whenever this isn’t the case, remember to explicitly specifyfetch=LAZY.

Most of the time, we would like to be able to easily navigate our associations in both directions.We do need a way to get thePublisher of a givenBook, but we would also like to be able to obtain all theBooks belonging to a given publisher.

To make this association bidirectional, we need to add a collection-valued attribute to thePublisher class, and annotate it@OneToMany.

Hibernate needs toproxy unfetched associations at runtime.Therefore, the many-valued side must be declared using an interface type likeSet orList, and never using a concrete type likeHashSet orArrayList.

To indicate clearly that this is a bidirectional association, and to reuse any mapping information already specified in theBook entity, we must use themappedBy annotation member to refer back toBook.publisher.

@EntityclassPublisher{@Id@GeneratedValueLongid;@OneToMany(mappedBy="publisher")Set<Book>books;...}

ThePublisher.books field is called theunowned side of the association.

Now, we passionatelyhate the stringly-typedmappedBy reference to the owning side of the association.Thankfully, theHibernate Processor gives us a way to make it abit more type safe:

@OneToMany(mappedBy=Book_.PUBLISHER)// get used to doing it this way!Set<Book>books;

We’re going to use this approach for the rest of the Short Guide.

To modify a bidirectional association, we must change theowning side.

Changes made to the unowned side of an association are never synchronized to the database.If we desire to change an association in the database, we must change it from the owning side.Here, we must setBook.publisher.

In fact, it’s often necessary to changeboth sides of a bidirectional association.For example, if the collectionPublisher.books was stored in the second-level cache, we must also modify the collection, to ensure that the second-level cache remains synchronized with the database.

That said, it’snot a hard requirement to update the unowned side, at least if you’re sure you know what you’re doing.

In principle Hibernatedoes allow you to have a unidirectional one-to-many, that is, a@OneToMany with no matching@ManyToOne on the other side.In practice, this mapping is unnatural, and just doesn’t work very well.Avoid it.

Here we’ve usedSet as the type of the collection, but Hibernate also allows the use ofList orCollection here, with almost no difference in semantics.In particular, theList may not contain duplicate elements, and its order will not be persistent.

@OneToMany(mappedBy=Book_.PUBLISHER)Collection<Book>books;

We’ll see how to map a collection with a persistent ordermuch later.

Set,List, orCollection?

A one-to-many association mapped to a foreign key can never contain duplicate elements, soSet seems like the most semantically correct Java collection type to use here, and so that’s the conventional practice in the Hibernate community.

The catch associated with using a set is that we must carefully ensure thatBook has a high-quality implementation ofequals() andhashCode().Now, that’s not necessarily a bad thing, since a qualityequals() is independently useful.

But what if we usedCollection orList instead?Then our code would be much less sensitive to howequals() andhashCode() were implemented.

In the past, we were perhaps too dogmatic in recommending the use ofSet.Now? I guess we’re happy to let you guys decide.In hindsight, we could have done more to make clear that this was always a viable option.

3.18. One-to-one (first way)

The simplest sort of one-to-one association is almost exactly like a@ManyToOne association, except that it maps to a foreign key column with aUNIQUE constraint.

Later, we’ll see how to map a one-to-one association to anassociation table.

A one-to-one association must be annotated@OneToOne:

@EntityclassAuthor{@Id@GeneratedValueLongid;@OneToOne(optional=false,fetch=LAZY)Personperson;...}

Here, theAuthor table has a foreign key column holding the identifier of the associatedPerson.

A one-to-one association often models a "type of" relationship.In our example, anAuthor is a type ofPerson.An alternative—and often more natural—way to represent "type of" relationships in Java is viaentity class inheritance.

We can make this association bidirectional by adding a reference back to theAuthor in thePerson entity:

@EntityclassPerson{@Id@GeneratedValueLongid;@OneToOne(mappedBy=Author_.PERSON)Authorauthor;...}

Person.author is the unowned side, because it’s the side markedmappedBy.

Lazy fetching for one-to-one associations

Notice that we did not declare the unowned end of the associationfetch=LAZY.That’s because:

  1. not everyPerson has an associatedAuthor, and

  2. the foreign key is held in the table mapped byAuthor, not in the table mapped byPerson.

Therefore, Hibernate can’t tell if the reference fromPerson toAuthor isnull without fetching the associatedAuthor.

On the other hand, ifeveryPerson was anAuthor, that is, if the association were non-optional, we would not have to consider the possibility ofnull references, and we would map it like this:

@OneToOne(optional=false,mappedBy=Author_.PERSON,fetch=LAZY)Authorauthor;

This is not the only sort of one-to-one association.

3.19. One-to-one (second way)

An arguably more elegant way to represent such a relationship is to share a primary key between the two tables.

To use this approach, theAuthor class must be annotated like this:

@EntityclassAuthor{@IdLongid;@OneToOne(optional=false,fetch=LAZY)@MapsIdPersonperson;...}

Notice that, compared with the previous mapping:

  • the@Id attribute is no longer a@GeneratedValue and,

  • instead, theauthor association is annotated@MapsId.

This lets Hibernate know that the association toPerson is the source of primary key values forAuthor.

Here, there’s no extra foreign key column in theAuthor table, since theid column holds the identifier ofPerson.That is, the primary key of theAuthor table does double duty as the foreign key referring to thePerson table.

ThePerson class doesn’t change.If the association is bidirectional, we annotate the unowned side@OneToOne(mappedBy = Author_.PERSON) just as before.

3.20. Many-to-many

A unidirectional many-to-many association is represented as a collection-valued attribute.It always maps to a separateassociation table in the database.

It tends to happen that a many-to-many association eventually turns out to be an entity in disguise.

Suppose we start with a nice clean many-to-many association betweenAuthor andBook.Later on, it’s quite likely that we’ll discover some additional information which comes attached to the association, so that the association table needs some extra columns.

For example, imagine that we needed to report the percentage contribution of each author to a book.That information naturally belongs to the association table.We can’t easily store it as an attribute ofBook, nor as an attribute ofAuthor.

When this happens, we need to change our Java model, usually introducing a new entity class which maps the association table directly.In our example, we might call this entity something likeBookAuthorship, and it would have@OneToMany associations to bothAuthor andBook, along with thecontribution attribute.

We can evade the disruption occasioned by such "discoveries" by simply avoiding the use of@ManyToMany right from the start.There’s little downside to representing every—or at leastalmost every—logical many-to-many association using an intermediate entity.

A many-to-many association must be annotated@ManyToMany:

@EntityclassBook{@Id@GeneratedValueLongid;@ManyToManySet<Author>authors;...}

If the association is bidirectional, we add a very similar-looking attribute toBook, but this time we must specifymappedBy to indicate that this is the unowned side of the association:

@EntityclassBook{@Id@GeneratedValueLongid;@ManyToMany(mappedBy=Author_.BOOKS)Set<Author>authors;...}

Remember, if we wish to the modify the collection we mustchange the owning side.

We’ve again usedSets to represent the association.As before, we have the option to useCollection orList.But in this case itdoes make a difference to the semantics of the association.

A many-to-many association represented as aCollection orList may contain duplicate elements.However, as before, the order of the elements is not persistent.That is, the collection is abag, not a set.

We don’t usually map collections withfetch=EAGER, since that usually leads to poor performance and fetching of unnecessary data.But this is especially clear in the case of many-to-many associations.We don’t much employ the word "never" when it comes to object/relational mappings, but here we will:never write@ManyToMany(fetch=EAGER) unless you’re deliberately looking for trouble.

3.21. Collections of basic values and embeddable objects

We’ve now seen the following kinds of entity attribute:

Kind of entity attributeKind of referenceMultiplicityExamples

Single-valued attribute of basic type

Non-entity

At most one

@Basic String name

Single-valued attribute of embeddable type

Non-entity

At most one

@Embedded Name name

Single-valued association

Entity

At most one

@ManyToOne Publisher publisher
@OneToOne Person person

Many-valued association

Entity

Zero or more

@OneToMany Set<Book> books
@ManyToMany Set<Author> authors

Scanning this taxonomy, you might ask: does Hibernate have multivalued attributes of basic or embeddable type?

Well, actually, we’ve already seen that it does, at least in two special cases.So first, letsrecall that JPA treatsbyte[] andchar[] arrays as basic types.Hibernate persists abyte[] orchar[] array to aVARBINARY orVARCHAR column, respectively.

But in this section we’re really concerned with casesother than these two special cases.So then,apart frombyte[] andchar[], does Hibernate have multivalued attributes of basic or embeddable type?

And the answer again is thatit does. Indeed, there are two different ways to handle such a collection, by mapping it:

  • to a column of SQLARRAY type (assuming the database has anARRAY type), or

  • to a separate table.

So we may expand our taxonomy with:

Kind of entity attributeKind of referenceMultiplicityExamples

byte[] andchar[] arrays

Non-entity

Zero or more

byte[] image
char[] text

Collection of basic-typed elements

Non-entity

Zero or more

@Array String[] names
@ElementCollection Set<String> names

Collection of embeddable elements

Non-entity

Zero or more

@ElementCollection Set<Name> names

There’s actually two new kinds of mapping here:@Array mappings, and@ElementCollection mappings.

These sorts of mappings are overused.

Thereare situations where we think it’s appropriate to use a collection of basic-typed values in our entity class.But such situations are rare.Almost every many-valued relationship should map to a foreign key association between separate tables.And almost every table should be mapped by an entity class.

The features we’re about to meet in the next two subsections are used much more often by beginners than they’re used by experts.So if you’re a beginner, you’ll save yourself same hassle by staying away from these features for now.

We’ll talk about@Array mappings first.

3.22. Collections mapped to SQL arrays

Let’s consider a calendar event which repeats on certain days of the week.We might represent this in ourEvent entity as an attribute of typeDayOfWeek[] orList<DayOfWeek>.Since the number of elements of this array or list is upper bounded by 7, this is a reasonable case for the use of anARRAY-typed column.It’s hard to see much value in storing this collection in a separate table.

Learning to not hate SQL arrays

For a long time, we thought arrays were a kind of weird and warty thing to add to the relational model, but recently we’ve come to realize that this view was overly closed-minded.Indeed, we might choose to view SQLARRAY types as a generalization ofVARCHAR andVARBINARY to generic "element" types.And from this point of view, SQL arrays look quite attractive, at least for certain problems.If we’re comfortable mappingbyte[] toVARBINARY(255), why would we shy away from mappingDayOfWeek[] toTINYINT ARRAY[7]?

Unfortunately, JPA doesn’t define a standard way to map SQL arrays, but here’s how we can do it in Hibernate:

@EntityclassEvent{@Id@GeneratedValueLongid;...@Array(length=7)DayOfWeek[]daysOfWeek;// stored as a SQL ARRAY type...}

The@Array annotation is optional—​it lets us specify an upper bound on the length of theARRAY column.By writing@Array(length=7) here, we specified that DDL should be generated with the column typeTINYINT ARRAY[7].

Just for fun, we used an enumerated type in the code above, but the array element time may be almost anybasic type.For example, the Java array typesString[],UUID[],double[],BigDecimal[],LocalDate[], andOffsetDateTime[] are all allowed, mapping to the SQL typesVARCHAR(n) ARRAY,UUID ARRAY,FLOAT(53) ARRAY,NUMERIC(p,s) ARRAY,DATE ARRAY, andTIMESTAMP(p) WITH TIME ZONE ARRAY, respectively.

Now for the gotcha: not every database has a SQLARRAY type, and some thatdo have anARRAY type don’t allow it to be used as a column type.

In particular, neither DB2 nor SQL Server have array-typed columns.On these databases, Hibernate falls back to something much worse: it uses Java serialization to encode the array to a binary representation, and stores the binary stream in aVARBINARY column.Quite clearly, this is terrible.You can ask Hibernate to do somethingslightly less terrible by annotating the attribute@JdbcTypeCode(SqlTypes.JSON), so that the array is serialized to JSON instead of binary format.But at this point it’s better to just admit defeat and use an@ElementCollection instead.

Alternatively, we could store this array or list in a separate table.

3.23. Collections mapped to a separate table

JPAdoes define a standard way to map a collection to an auxiliary table: the@ElementCollection annotation.

@EntityclassEvent{@Id@GeneratedValueLongid;...@ElementCollectionDayOfWeek[]daysOfWeek;// stored in a dedicated table...}

Actually, we shouldn’t use an array here, since array types can’t beproxied, and so the JPA specification doesn’t even say they’re supported.Instead, we should useSet,List, orMap.

@EntityclassEvent{@Id@GeneratedValueLongid;...@ElementCollectionList<DayOfWeek>daysOfWeek;// stored in a dedicated table...}

Here, each collection element is stored as a separate row of the auxiliary table.By default, this table has the following definition:

createtableEvent_daysOfWeek(Event_idbigintnotnull,daysOfWeektinyintcheck(daysOfWeekbetween0and6),daysOfWeek_ORDERintegernotnull,primarykey(Event_id,daysOfWeek_ORDER))

Which is fine, but it’s still a mapping we prefer to avoid.

@ElementCollection is one of our least-favorite features of JPA.Even the name of the annotation is bad.

The code above results in a table with three columns:

  • a foreign key of theEvent table,

  • aTINYINT encoding theenum, and

  • anINTEGER encoding the ordering of elements in the array.

Instead of a surrogate primary key, it has a composite key comprising the foreign key ofEvent and the order column.

When—inevitably—we find that we need to add a fourth column to that table, our Java code must change completely.Most likely, we’ll realize that we need to add a separate entity after all.So this mapping isn’t very robust in the face of minor changes to our data model.

There’s much more we could say about "element collections", but we won’t say it, because we don’t want to hand you the gun you’ll shoot your foot with.

3.24. Summary of annotations

Let’s pause to remember the annotations we’ve met so far.

Table 19. Declaring entities and embeddable types
AnnotationPurposeJPA-standard

@Entity

Declare an entity class

@MappedSuperclass

Declare a non-entity class with mapped attributes inherited by an entity

@Embeddable

Declare an embeddable type

@IdClass

Declare the identifier class for an entity with multiple@Id attributes

Table 20. Declaring basic and embedded attributes
AnnotationPurposeJPA-standard

@Id

Declare a basic-typed identifier attribute

@Version

Declare a version attribute

@Basic

Declare a basic attribute

Default

@EmbeddedId

Declare an embeddable-typed identifier attribute

@Embedded

Declare an embeddable-typed attribute

Inferred

@Enumerated

Declare anenum-typed attribute and specify how it is encoded

Inferred

@Array

Declare that an attribute maps to a SQLARRAY, and specify the length

Inferred

@ElementCollection

Declare that a collection is mapped to a dedicated table

Table 21. Converters and compositional basic types
AnnotationPurposeJPA-standard

@Converter

Register anAttributeConverter

@Convert

Apply a converter to an attribute

@JavaType

Explicitly specify an implementation ofJavaType for a basic attribute

@JdbcType

Explicitly specify an implementation ofJdbcType for a basic attribute

@JdbcTypeCode

Explicitly specify a JDBC type code used to determine theJdbcType for a basic attribute

@JavaTypeRegistration

Register aJavaType for a given Java type

@JdbcTypeRegistration

Register aJdbcType for a given JDBC type code

Table 22. System-generated identifiers
AnnotationPurposeJPA-standard

@GeneratedValue

Specify that an identifier is system-generated

@SequenceGenerator

Define an id generator backed by a database sequence

@TableGenerator

Define an id generated backed by a database table

@IdGeneratorType

Declare an annotation that associates a customGenerator with each@Id attribute it annotates

@ValueGenerationType

Declare an annotation that associates a customGenerator with each@Basic attribute it annotates

Table 23. Declaring entity associations
AnnotationPurposeJPA-standard

@ManyToOne

Declare the single-valued side of a many-to-one association (the owning side)

@OneToMany

Declare the many-valued side of a many-to-one association (the unowned side)

@ManyToMany

Declare either side of a many-to-many association

@OneToOne

Declare either side of a one-to-one association

@MapsId

Declare that the owning side of a@OneToOne association maps the primary key column

Phew!That’s already a lot of annotations, and we have not even started with the annotations for O/R mapping!

3.25.equals() andhashCode()

Entity classes should overrideequals() andhashCode(), especially when associations arerepresented as sets.

People new to Hibernate or JPA are often confused by exactly which fields should be included in thehashCode().And people with more experience often argue quite religiously that one or another approach is the only right way.The truth is, there’s no unique right way to do it, but there are some constraints.So please keep the following principles in mind:

  • You should not include a mutable field in the hashcode, since that would require rehashing every collection containing the entity whenever the field is mutated.

  • It’s not completely wrong to include a generated identifier (surrogate key) in the hashcode, but since the identifier is not generated until the entity instance is made persistent, you must take great care to not add it to any hashed collection before the identifier is generated. We therefore advise against including any database-generated field in the hashcode.

It’s OK to include any immutable, non-generated field in the hashcode.

We therefore recommend identifying anatural key for each entity, that is, a combination of fields that uniquely identifies an instance of the entity, from the perspective of the data model of the program. The natural key should correspond to a unique constraint on the database, and to the fields which are included inequals() andhashCode().

In this example, theequals() andhashCode() methods agree with the@NaturalId annotation:

@EntityclassBook{@Id@GeneratedValueLongid;@NaturalId@Basic(optional=false)Stringisbn;StringgetIsbn(){returnisbn;}...@Overridepublicbooleanequals(Objectother){returnotherinstanceofBook// check type with instanceof, not getClass()&&((Book)other).getIsbn().equals(isbn);// compare natural ids}@OverridepublicinthashCode(){returnisbn.hashCode();// hashcode based on the natural id}}

That said, an implementation ofequals() andhashCode() based on the generated identifier of the entity can workif you’re careful.

Your implementation ofequals() must be written to accommodate the possibility that the object passed to theequals() might be aproxy.Therefore, you should useinstanceof, notgetClass() to check the type of the argument, and should access fields of the passed entity via its accessor methods.

4. Object/relational mapping

Given a domain model—that is, a collection of entity classes decorated with all the fancy annotations wejust met in the previous chapter—Hibernate will happily go away and infer a complete relational schema, and evenexport it to your database if you ask politely.

The resulting schema will be entirely sane and reasonable, though if you look closely, you’ll find some flaws.For example, by default, everyVARCHAR column will have the same length,VARCHAR(255).

But the process I just described—which we calltop down mapping—simply doesn’t fit the most common scenario for the use of O/R mapping.It’s only rarely that the Java classes precede the relational schema.Usually,we already have a relational schema, and we’re constructing our domain model around the schema.This is calledbottom up mapping.

Developers often refer to a pre-existing relational database as "legacy" data.This tends to conjure images of bad old "legacy apps" written in COBOL or something.But legacy data is valuable, and learning to work with it is important.

Especially when mapping bottom up, we often need to customize the inferred object/relational mappings.This is a somewhat tedious topic, and so we don’t want to spend too many words on it.Instead, we’ll quickly skim the most important mapping annotations.

Hibernate SQL case convention

Computers have had lowercase letters for rather a long time now.Most developers learned long ago that text written in MixedCase, camelCase, or even snake_case is easier to read than text written in SHOUTYCASE.This is just as true of SQL as it is of any other language.

Therefore, for over twenty years, the convention on the Hibernate project has been that:

  • query language identifiers are written inlowercase,

  • table names are written inMixedCase, and

  • column names are written incamelCase.

That is to say, we simply adopted Java’s excellent conventions and applied them to SQL.

Now, there’s no way we can force you to follow this convention, even if we wished to.Hell, you can easily write aPhysicalNamingStrategy which makes table and column names ALL UGLY AND SHOUTY LIKE THIS IF YOU PREFER.But,by default, it’s the convention Hibernate follows, and it’s frankly a pretty reasonable one.

4.1. Mapping entity inheritance hierarchies

InEntity class inheritance we saw that entity classes may exist within an inheritance hierarchy.There’s three basic strategies for mapping an entity hierarchy to relational tables.Let’s put them in a table, so we can more easily compare the points of difference between them.

Table 24. Entity inheritance mapping strategies
StrategyMappingPolymorphic queriesConstraintsNormalizationWhen to use it

SINGLE_TABLE

Map every class in the hierarchy to the same table, and uses the value of adiscriminator column to determine which concrete class each row represents.

To retrieve instances of a given class, we only need to query the one table.

Attributes declared by subclasses map to columns withoutNOT NULL constraints. 💀

Any association may have aFOREIGN KEY constraint. 🤓

Subclass data is denormalized. 🧐

Works well when subclasses declare few or no additional attributes.

JOINED

Map every class in the hierarchy to a separate table, but each table only maps the attributes declared by the class itself.

Optionally, a discriminator column may be used.

To retrieve instances of a given class, we mustJOIN the table mapped by the class with:

  • all tables mapped by its superclasses and

  • all tables mapped by its subclasses.

Any attribute may map to a column with aNOT NULL constraint. 🤓

Any association may have aFOREIGN KEY constraint. 🤓

The tables are normalized. 🤓

The best option when we care a lot about constraints and normalization.

TABLE_PER_CLASS

Map every concrete class in the hierarchy to a separate table, but denormalize all inherited attributes into the table.

To retrieve instances of a given class, we must take aUNION over the table mapped by the class and the tables mapped by its subclasses.

Associations targeting a superclass cannot have a correspondingFOREIGN KEY constraint in the database. 💀💀

Any attribute may map to a column with aNOT NULL constraint. 🤓

Superclass data is denormalized. 🧐

Not very popular.

From a certain point of view, competes with@MappedSuperclass.

The three mapping strategies are enumerated byInheritanceType.We specify an inheritance mapping strategy using the@Inheritance annotation.

For mappings with adiscriminator column, we should:

  • specify the discriminator column name and type by annotating the root entity@DiscriminatorColumn, and

  • specify the values of this discriminator by annotating each entity in the hierarchy@DiscriminatorValue.

For single table inheritance we always need a discriminator:

@Entity@DiscriminatorColumn(discriminatorType=CHAR,name="kind")@DiscriminatorValue('P')classPerson{...}@Entity@DiscriminatorValue('A')classAuthor{...}

We don’t need to explicitly specify@Inheritance(strategy=SINGLE_TABLE), since that’s the default.

ForJOINED inheritance we don’t need a discriminator:

@Entity@Inheritance(strategy=JOINED)classPerson{...}@EntityclassAuthor{...}

However, we can add a discriminator column if we like, and in that case the generated SQL for polymorphic queries will be slightly simpler.

Similarly, forTABLE_PER_CLASS inheritance we have:

@Entity@Inheritance(strategy=TABLE_PER_CLASS)classPerson{...}@EntityclassAuthor{...}

Hibernate doesn’t allow discriminator columns forTABLE_PER_CLASS inheritance mappings, since they would make no sense, and offer no advantage.

Notice that in this last case, a polymorphic association like:

@ManyToOnePersonperson;

is a bad idea, since it’s impossible to create a foreign key constraint that targets both mapped tables.

4.2. Mapping to tables

The following annotations specify exactly how elements of the domain model map to tables of the relational model:

Table 25. Annotations for mapping tables
AnnotationPurpose

@Table

Map an entity class to its primary table

@SecondaryTable

Define a secondary table for an entity class

@JoinTable

Map a many-to-many or many-to-one association to its association table

@CollectionTable

Map an@ElementCollection to its table

The first two annotations are used to map an entity to itsprimary table and, optionally, one or moresecondary tables.

4.3. Mapping entities to tables

By default, an entity maps to a single table, which may be specified using@Table:

@Entity@Table(name="People")classPerson{...}

However, the@SecondaryTable annotation allows us to spread its attributes across multiplesecondary tables.

@Entity@Table(name="Books")@SecondaryTable(name="Editions")classBook{...}

The@Table annotation can do more than just specify a name:

Table 26.@Table annotation members
Annotation memberPurpose

name

The name of the mapped table

schema 💀

The schema to which the table belongs

catalog 💀

The catalog to which the table belongs

uniqueConstraints

One or more@UniqueConstraint annotations declaring multi-column unique constraints

indexes

One or more@Index annotations each declaring an index

check

One or more@CheckConstraint annotations declaring multi-column check constraints

comment

A DDL comment

It only makes sense to explicitly specify theschema in annotations if the domain model is spread across multiple schemas.

Otherwise, it’s a bad idea to hardcode the schema (or catalog) in a@Table annotation.Instead:

  • set the configuration propertyhibernate.default_schema (orhibernate.default_catalog), or

  • simply specify the schema in the JDBC connection URL.

The@SecondaryTable annotation is even more interesting:

Table 27.@SecondaryTable annotation members
Annotation memberPurpose

name

The name of the mapped table

schema 💀

The schema to which the table belongs

catalog 💀

The catalog to which the table belongs

uniqueConstraints

One or more@UniqueConstraint annotations declaring multi-column unique constraints

indexes

One or more@Index annotations each declaring an index

pkJoinColumns

One or more@PrimaryKeyJoinColumn annotations, specifyingprimary key column mappings

foreignKey

A@ForeignKey annotation specifying the name of theFOREIGN KEY constraint on the@PrimaryKeyJoinColumns

check

One or more@CheckConstraint annotations declaring multi-column check constraints

comment

A DDL comment

Using@SecondaryTable on a subclass in aSINGLE_TABLE entity inheritance hierarchy gives us a sort of mix ofSINGLE_TABLE withJOINED inheritance.

4.4. Mapping associations to tables

The@JoinTable annotation specifies anassociation table, that is, a table holding foreign keys of both associated entities.This annotation is usually used with@ManyToMany associations:

@EntityclassBook{...@ManyToMany@JoinTable(name="BooksAuthors")Set<Author>authors;...}

But it’s even possible to use it to map a@ManyToOne or@OneToOne association to an association table.

@EntityclassBook{...@ManyToOne(fetch=LAZY)@JoinTable(name="BookPublisher")Publisherpublisher;...}

Here, there should be aUNIQUE constraint on one of the columns of the association table.

@EntityclassAuthor{...@OneToOne(optional=false,fetch=LAZY)@JoinTable(name="AuthorPerson")Personauthor;...}

Here, there should be aUNIQUE constraint onboth columns of the association table.

Table 28.@JoinTable annotation members
Annotation memberPurpose

name

The name of the mapped association table

schema 💀

The schema to which the table belongs

catalog 💀

The catalog to which the table belongs

uniqueConstraints

One or more@UniqueConstraint annotations declaring multi-column unique constraints

indexes

One or more@Index annotations each declaring an index

joinColumns

One or more@JoinColumn annotations, specifyingforeign key column mappings to the table of the owning side

inverseJoinColumns

One or more@JoinColumn annotations, specifyingforeign key column mappings to the table of the unowned side

foreignKey

A@ForeignKey annotation specifying the name of theFOREIGN KEY constraint on thejoinColumnss

inverseForeignKey

A@ForeignKey annotation specifying the name of theFOREIGN KEY constraint on theinverseJoinColumnss

check

One or more@CheckConstraint annotations declaring multi-column check constraints

comment

A DDL comment

To better understand these annotations, we must first discuss column mappings in general.

4.5. Mapping to columns

These annotations specify how elements of the domain model map to columns of tables in the relational model:

Table 29. Annotations for mapping columns
AnnotationPurpose

@Column

Map an attribute to a column

@JoinColumn

Map an association to a foreign key column

@PrimaryKeyJoinColumn

Map the primary key used to join a secondary table with its primary, or a subclass table inJOINED inheritance with its root class table

@OrderColumn

Specifies a column that should be used to maintain the order of aList.

@MapKeyColumn

Specified a column that should be used to persist the keys of aMap.

We’ll come back to the last two annotations much later, inOrdered and sorted collections and map keys.

We use the@Column annotation to map basic attributes.

4.6. Mapping basic attributes to columns

The@Column annotation is not only useful for specifying the column name.

Table 30.@Column annotation members
Annotation memberPurpose

name

The name of the mapped column

table

The name of the table to which this column belongs

length

The length of aVARCHAR,CHAR, orVARBINARY column type

precision

The decimal digits of precision of aFLOAT,DECIMAL, orNUMERIC type

scale

The scale of aDECIMAL orNUMERIC column type, the digits of precision that occur to the right of the decimal point

secondPrecision

The digits of precision occurring to the right of the decimal point in the seconds field of aTIME, orTIMESTAMP column type

unique

Whether the column has aUNIQUE constraint

nullable

Whether the column has aNOT NULL constraint

insertable

Whether the column should appear in generated SQLINSERT statements

updatable

Whether the column should appear in generated SQLUPDATE statements

columnDefinition 💀

A DDL fragment that should be used to declare the column

check

One or more@CheckConstraint annotations declaring single-column check constraints

comment

A DDL comment

We no longer recommend the use ofcolumnDefinition since it results in unportable DDL.Hibernate has much better ways to customize the generated DDL using techniques that result in portable behavior across different databases.

Here we see four different ways to use the@Column annotation:

@Entity@Table(name="Books")@SecondaryTable(name="Editions")classBook{@Id@GeneratedValue@Column(name="bookId")// customize column nameLongid;@Column(length=100,nullable=false)// declare column as VARCHAR(100) NOT NULLStringtitle;@Column(length=17,unique=true,nullable=false)// declare column as VARCHAR(17) NOT NULL UNIQUEStringisbn;@Column(table="Editions",updatable=false)// column belongs to the secondary table, and is never updatedintedition;}

We don’t use@Column to map associations.

4.7. Mapping associations to foreign key columns

The@JoinColumn annotation is used to customize a foreign key column.

Table 31.@JoinColumn annotation members
Annotation memberPurpose

name

The name of the mapped foreign key column

table

The name of the table to which this column belongs

referencedColumnName

The name of the column to which the mapped foreign key column refers

unique

Whether the column has aUNIQUE constraint

nullable

Whether the column has aNOT NULL constraint

insertable

Whether the column should appear in generated SQLINSERT statements

updatable

Whether the column should appear in generated SQLUPDATE statements

columnDefinition 💀

A DDL fragment that should be used to declare the column

foreignKey

A@ForeignKey annotation specifying the name of theFOREIGN KEY constraint

check

One or more@CheckConstraint annotations declaring single-column check constraints

comment

A DDL comment

A foreign key column doesn’t necessarily have to refer to the primary key of the referenced table.It’s quite acceptable for the foreign key to refer to any other unique key of the referenced entity, even to a unique key of a secondary table.

Here we see how to use@JoinColumn to define a@ManyToOne association mapping a foreign key column which refers to the@NaturalId ofBook:

@Entity@Table(name="Items")classItem{...@ManyToOne(optional=false)// implies nullable=false@JoinColumn(name="bookIsbn",referencedColumnName="isbn",// a reference to a non-PK columnforeignKey=@ForeignKey(name="ItemsToBooksBySsn"))// supply a name for the FK constraintBookbook;...}

In case this is confusing:

  • bookIsbn is the name of the foreign key column in theItems table,

  • it refers to a unique keyisbn in theBooks table, and

  • it has a foreign key constraint namedItemsToBooksBySsn.

Note that theforeignKey member is completely optional and only affects DDL generation.

If you don’t supply an explicit name using@ForeignKey, Hibernate will generate a quite ugly name.The reason for this is that the maximum length of foreign key names on some databases is extremely constrained, and we need to avoid collisions.To be fair, this is perfectly fine if you’re only using the generated DDL for testing.You can customize the generated constraint names by writing your ownImplicitNamingStrategy.

For composite foreign keys we might have multiple@JoinColumn annotations:

@Entity@Table(name="Items")classItem{...@ManyToOne(optional=false)@JoinColumn(name="bookIsbn",referencedColumnName="isbn")@JoinColumn(name="bookPrinting",referencedColumnName="printing")Bookbook;...}

If we need to specify the@ForeignKey, this starts to get a bit messy:

@Entity@Table(name="Items")classItem{...@ManyToOne(optional=false)@JoinColumns(value={@JoinColumn(name="bookIsbn",referencedColumnName="isbn"),@JoinColumn(name="bookPrinting",referencedColumnName="printing")},foreignKey=@ForeignKey(name="ItemsToBooksBySsn"))Bookbook;...}

For associations mapped to a@JoinTable, fetching the association requires two joins, and so we must declare the@JoinColumns inside the@JoinTable annotation:

@EntityclassBook{@Id@GeneratedValueLongid;@ManyToMany@JoinTable(joinColumns=@JoinColumn(name="bookId"),inverseJoinColumns=@joinColumn(name="authorId"),foreignKey=@ForeignKey(name="BooksToAuthors"))Set<Author>authors;...}

Again, theforeignKey member is optional.

For mapping a@OneToOne associationto a primary key with@MapsId, Hibernate lets us use either@JoinColumn or@PrimaryKeyJoinColumn.

@EntityclassAuthor{@IdLongid;@OneToOne(optional=false,fetch=LAZY)@MapsId@PrimaryKeyJoinColumn(name="personId")Personauthor;...}

Arguably, the use of@PrimaryKeyJoinColumn is clearer.

4.8. Mapping primary key joins between tables

The@PrimaryKeyJoinColumn is a special-purpose annotation for mapping:

  • the primary key column of a@SecondaryTable—which is also a foreign key referencing the primary table, or

  • the primary key column of the primary table mapped by a subclass in aJOINED inheritance hierarchy—which is also a foreign key referencing the primary table mapped by the root entity.

Table 32.@PrimaryKeyJoinColumn annotation members
Annotation memberPurpose

name

The name of the mapped foreign key column

referencedColumnName

The name of the column to which the mapped foreign key column refers

columnDefinition 💀

A DDL fragment that should be used to declare the column

foreignKey

A@ForeignKey annotation specifying the name of theFOREIGN KEY constraint

When mapping a subclass table primary key, we place the@PrimaryKeyJoinColumn annotation on the entity class:

@Entity@Table(name="People")@Inheritance(strategy=JOINED)classPerson{...}@Entity@Table(name="Authors")@PrimaryKeyJoinColumn(name="personId")// the primary key of the Authors tableclassAuthor{...}

But to map a secondary table primary key, the@PrimaryKeyJoinColumn annotation must occur inside the@SecondaryTable annotation:

@Entity@Table(name="Books")@SecondaryTable(name="Editions",pkJoinColumns=@PrimaryKeyJoinColumn(name="bookId"))// the primary key of the Editions tableclassBook{@Id@GeneratedValue@Column(name="bookId")// the name of the primary key of the Books tableLongid;...}

4.9. Column lengths and adaptive column types

Hibernate automatically adjusts the column type used in generated DDL based on the column length specified by the@Column annotation.So we don’t usually need to explicitly specify that a column should be of typeTEXT orCLOB—or worry about the parade ofTINYTEXT,MEDIUMTEXT,TEXT,LONGTEXT types on MySQL—because Hibernate automatically selects one of those types if required to accommodate a string of thelength we specify.

The constant values defined in the classLength are very helpful here:

Table 33. Predefined column lengths
ConstantValueDescription

DEFAULT

255

The default length of aVARCHAR orVARBINARY column when none is explicitly specified

LONG

32600

The largest column length for aVARCHAR orVARBINARY that is allowed on every database Hibernate supports

LONG16

32767

The maximum length that can be represented using 16 bits (but this length is too large for aVARCHAR orVARBINARY column on for some database)

LONG32

2147483647

The maximum length for a Java string

We can use these constants in the@Column annotation:

@Column(length=LONG)Stringtext;@Column(length=LONG32)byte[]binaryData;

This is usually all you need to do to make use of large object types in Hibernate.

4.10. LOBs

JPA provides a@Lob annotation which specifies that a field should be persisted as aBLOB orCLOB.

Semantics of the@Lob annotation

What the spec actually says is that the field should be persisted

…​as a large object to a database-supported large object type.

It’s quite unclear what this means, and the spec goes on to say that

…​the treatment of theLob annotation is provider-dependent…​

which doesn’t help much.

Hibernate interprets this annotation in what we think is the most reasonable way.In Hibernate, an attribute annotated@Lob will be written to JDBC using thesetClob() orsetBlob() method ofPreparedStatement, and will be read from JDBC using thegetClob() orgetBlob() method ofResultSet.

Now, the use of these JDBC methods is usually unnecessary!JDBC drivers are perfectly capable of converting betweenString andCLOB or betweenbyte[] andBLOB.So unless you specifically need to use these JDBC LOB APIs, youdon’t need the@Lob annotation.

Instead, as we just saw inColumn lengths and adaptive column types, all you need is to specify a large enough columnlength to accommodate the data you plan to write to that column.

You should usually write this:

@Column(length=LONG32)// good, correct column type inferredStringtext;

instead of this:

@Lob// almost always unnecessaryStringtext;

This is particularly true for PostgreSQL.

Unfortunately, the driver for PostgreSQL doesn’t allowBYTEA orTEXT columns to be read via the JDBC LOB APIs.

This limitation of the Postgres driver has resulted in a whole cottage industry of bloggers and stackoverflow question-answerers recommending convoluted ways to hack the HibernateDialect for Postgres to allow an attribute annotated@Lob to be written usingsetString() and read usinggetString().

But simply removing the@Lob annotation has exactly the same effect.

Conclusion:

  • on PostgreSQL,@Lob always means theOID type,

  • @Lob should never be used to map columns of typeBYTEA orTEXT, and

  • please don’t believe everything you read on stackoverflow.

Finally, as an alternative, Hibernate lets you declare an attribute of typejava.sql.Blob orjava.sql.Clob.

@EntityclassBook{...Clobtext;BlobcoverArt;....}

The advantage is that ajava.sql.Clob orjava.sql.Blob can in principle index up to 263 characters or bytes, much more data than you can fit in a JavaString orbyte[] array (or in your computer).

To assign a value to these fields, we’ll need to use aLobHelper.We can get one from theSession:

LobHelperhelper=session.getLobHelper();book.text=helper.createClob(text);book.coverArt=helper.createBlob(image);

In principle, theBlob andClob objects provide efficient ways to read or stream LOB data from the server.

Bookbook=session.find(Book.class,bookId);Stringtext=book.text.getSubString(1,textLength);InputStreambytes=book.coverArt.getBinaryStream();

Of course, the behavior here depends very much on the JDBC driver, and so we really can’t promise that this is a sensible thing to do on your database.

4.11. Mapping embeddable types to UDTs or to JSON

There’s a couple of alternative ways to represent an embeddable type on the database side.

Embeddables as UDTs

First, a really nice option, at least in the case of Java record types, and for databases which supportuser-defined types (UDTs), is to define a UDT which represents the record type.Hibernate 6 makes this really easy.Just annotate the record type, or the attribute which holds a reference to it, with the new@Struct annotation:

@Embeddable@Struct(name="PersonName")recordName(StringfirstName,StringmiddleName,StringlastName){}
@EntityclassPerson{...Namename;...}

This results in the following UDT:

createtypePersonNameas(firstNamevarchar(255),middleNamevarchar(255),lastNamevarchar(255))

And thename column of theAuthor table will have the typePersonName.

Embeddables to JSON

A second option that’s available is to map the embeddable type to aJSON (orJSONB) column.Now, this isn’t something we would exactlyrecommend if you’re defining a data model from scratch, but it’s at least useful for mapping pre-existing tables with JSON-typed columns.Since embeddable types are nestable, we can map some JSON formats this way, and even query JSON properties using HQL.

At this time, JSON arrays are not supported!

To map an attribute of embeddable type to JSON, we must annotate the attribute@JdbcTypeCode(SqlTypes.JSON), instead of annotating the embeddable type.But the embeddable typeName should still be annotated@Embeddable if we want to query its attributes using HQL.

@EmbeddablerecordName(StringfirstName,StringmiddleName,StringlastName){}
@EntityclassPerson{...@JdbcTypeCode(SqlTypes.JSON)Namename;...}

We also need to add Jackson or an implementation of JSONB—for example, Yasson—to our runtime classpath.To use Jackson we could add this line to our Gradle build:

runtimeOnly'com.fasterxml.jackson.core:jackson-databind:{jacksonVersion}'

Now thename column of theAuthor table will have the typejsonb, and Hibernate will automatically use Jackson to serialize aName to and from JSON format.

4.12. Summary of SQL column type mappings

So, as we’ve seen, there are quite a few annotations that affect the mapping of Java types to SQL column types in DDL.Here we summarize the ones we’ve just seen in the second half of this chapter, along with some we already mentioned in earlier chapters.

Table 34. Annotations for mapping SQL column types
AnnotationInterpretation

@Enumerated,@EnumeratedValue

Specify how anenum type should be persisted

@Nationalized

Use anationalized character type:NCHAR,NVARCHAR, orNCLOB

@Lob 💀

UseJDBC LOB APIs to read and write the annotated attribute

@Array

Map a collection to aSQLARRAY type of the specified length

@Struct

Map anembeddable to a SQL UDT with the given name

@TimeZoneStorage

Specify how thetime zone information should be persisted

@JdbcType or@JdbcTypeCode

Use an implementation ofJdbcType to map an arbitrary SQL type

@Collate

Specify a collation for a column

In addition, there aresome configuration properties which have aglobal effect on how basic types map to SQL column types:

Table 35. Type mapping settings
Configuration property namePurpose

hibernate.use_nationalized_character_data

Enable use ofnationalized character types by default

hibernate.type.preferred_boolean_jdbc_type

Specify the default SQL column type for storing aboolean

hibernate.type.preferred_uuid_jdbc_type

Specify the default SQL column type for storing aUUID

hibernate.type.preferred_duration_jdbc_type

Specify the default SQL column type for storing aDuration

hibernate.type.preferred_instant_jdbc_type

Specify the default SQL column type for storing anInstant

hibernate.timezone.default_storage

Specify the default strategy forstoring time zone information

hibernate.type.prefer_native_enum_types

Usenamed enum types on PostgreSQL and Oracle

Earlier, we saw how to use these settings to control the default mappings forInstant andDuration.

These areglobal settings and thus quite clumsy.We recommend against messing with any of these settings unless you have a really good reason for it.

There’s one more topic we would like to cover in this chapter.

4.13. Mapping to formulas

Hibernate lets us map an attribute of an entity to a SQL formula involving columns of the mapped table.Thus, the attribute is a sort of "derived" value.

Table 36. Annotations for mapping formulas
AnnotationPurpose

@Formula

Map an attribute to a SQL formula

@JoinFormula

Map an association to a SQL formula

@DiscriminatorFormula

Use a SQL formula as the discriminator insingle table inheritance.

For example:

@EntityclassOrder{...@Column(name="sub_total",scale=2,precision=8)BigDecimalsubTotal;@Column(name="tax",scale=4,precision=4)BigDecimaltaxRate;@Formula("sub_total * (1.0 + tax)")BigDecimaltotalWithTax;...}

The formula is evaluated every time the entity is read from the database.

4.14. Derived Identity

An entity has aderived identity if it inherits part of its primary key from an associated "parent" entity.We’ve already met a kind of degenerate case ofderived identity when we talked aboutone-to-one associations with a shared primary key.

But a@ManyToOne association may also form part of a derived identity.That is to say, there could be a foreign key column or columns included as part of the composite primary key.There’s three different ways to represent this situation on the Java side of things:

  • using@IdClass without@MapsId,

  • using@IdClass with@MapsId, or

  • using@EmbeddedId with@MapsId.

Let’s suppose we have aParent entity class defined as follows:

@EntityclassParent{@IdLongparentId;...}

TheparentId field holds the primary key of theParent table, which will also form part of the composite primary key of everyChild belonging to theParent.

First way

In the first, slightly simpler approach, we define an@IdClass to represent the primary key ofChild:

classDerivedId{Longparent;StringchildId;// constructors, equals, hashcode, etc...}

And aChild entity class with a@ManyToOne association annotated@Id:

@Entity@IdClass(DerivedId.class)classChild{@IdStringchildId;@Id@ManyToOne@JoinColumn(name="parentId")Parentparent;...}

Then the primary key of theChild table comprises the columns(childId,parentId).

Second way

This is fine, but sometimes it’s nice to have a field for each element of the primary key.We may use the@MapsId annotation we metearlier:

@Entity@IdClass(DerivedId.class)classChild{@IdLongparentId;@IdStringchildId;@ManyToOne@MapsId(Child_.PARENT_ID)// typesafe reference to Child.parentId@JoinColumn(name="parentId")Parentparent;...}

We’re using the approach we sawpreviously to refer to theparentId property ofChild in a typesafe way.

Note that we must place column mapping information on the association annotated@MapsId, not on the@Id field.

We must slightly modify our@IdClass so that field names align:

classDerivedId{LongparentId;StringchildId;// constructors, equals, hashcode, etc...}

Third way

The third alternative is to redefine our@IdClass as an@Embeddable.We don’t actually need to change theDerivedId class, but we do need to add the annotation.

@EmbeddableclassDerivedId{LongparentId;StringchildId;// constructors, equals, hashcode, etc...}

Then we may use@EmbeddedId inChild:

@EntityclassChild{@EmbeddedIdDerivedIdid;@ManyToOne@MapsId(DerivedId_.PARENT_ID)// typesafe reference to DerivedId.parentId@JoinColumn(name="parentId")Parentparent;...}

Thechoice between@IdClass and@EmbeddedId boils down to taste.The@EmbeddedId is perhaps a little DRYer.

4.15. Adding constraints

Database constraints are important.Even if you’re sure that your program has no bugs 🧐, it’s probably not the only program with access to the database.Constraints help ensure that different programs (and human administrators) play nicely with each other.

Hibernate adds certain constraints to generated DDL automatically: primary key constraints, foreign key constraints, and some unique constraints.But it’s common to need to:

  • add additional unique constraints,

  • add check constraints, or

  • customize the name of a foreign key constraint.

We’vealready seen how to use@ForeignKey to specify the name of a foreign key constraint.

There are two ways to add a unique constraint to a table:

  • using@Column(unique=true) to indicate a single-column unique key, or

  • using the@UniqueConstraint annotation to define a uniqueness constraint on a combination of columns.

@Entity@Table(uniqueConstraints=@UniqueConstraint(columnNames={"title","year","publisher_id"}))classBook{...}

This annotation looks a bit ugly perhaps, but it’s actually useful even as documentation.

The@Check annotation adds a check constraint to the table.

@Entity@Check(name="ValidISBN",constraints="length(isbn)=13")classBook{...}

The@Check annotation is commonly used at the field level:

@Id@Check(constraints="length(isbn)=13")Stringisbn;

5. Interacting with the database

To interact with the database, that is, to execute queries, or to insert, update, or delete data, we need an instance of one of the following objects:

  • a JPAEntityManager,

  • a HibernateSession, or

  • a HibernateStatelessSession.

TheSession interface extendsEntityManager, and so the only difference between the two interfaces is thatSession offers a few more operations.

Actually, in Hibernate, everyEntityManager is aSession, and you can narrow it like this:

Sessionsession=entityManager.unwrap(Session.class);

An instance ofSession (or ofEntityManager) is astateful session.It mediates the interaction between your program and the database via operations on apersistence context.

In this chapter, we’re not going to talk much aboutStatelessSession.We’ll come back tothis very useful API when we talk about performance.What you need to know for now is that a stateless session doesn’t have a persistence context.

Still, we should let you know that some people prefer to useStatelessSession everywhere.It’s a simpler programming model, and lets the developer interact with the database moredirectly.

Stateful sessions certainly have their advantages, but they’re more difficult to reason about, and when something goes wrong, the error messages can be more difficult to understand.

5.1. Persistence contexts

A persistence context is a sort of cache; we sometimes call it the "first-level cache", to distinguish it from thesecond-level cache.For every entity instance read from the database within the scope of a persistence context, and for every new entity made persistent within the scope of the persistence context, the context holds a unique mapping from the identifier of the entity instance to the instance itself.

Thus, an entity instance may be in one of three states with respect to a given persistence context:

  1. transient — never persistent, and not associated with the persistence context,

  2. persistent — currently associated with the persistence context, or

  3. detached — previously persistent in another session, but not currently associated withthis persistence context.

Entity lifecycle

At any given moment, an instance may be associated with at most one persistence context.

The lifetime of a persistence context usually corresponds to the lifetime of a transaction, though it’s possible to have a persistence context that spans several database-level transactions that form a single logical unit of work.

A persistence context—that is, aSession orEntityManager—absolutely positivelymust not be shared between multiple threads or between concurrent transactions.

If you accidentally leak a session across threads, you will suffer.

Container-managed persistence contexts

In a container environment, the lifecycle of a persistence context scoped to the transaction will usually be managed for you.

There are several reasons we like persistence contexts.

  1. They help avoiddata aliasing: if we modify an entity in one section of code, then other code executing within the same persistence context will see our modification.

  2. They enableautomatic dirty checking: after modifying an entity, we don’t need to perform any explicit operation to ask Hibernate to propagate that change back to the database.Instead, the change will be automatically synchronized with the database when the session isflushed.

  3. They can improve performance by avoiding a trip to the database when a given entity instance is requested repeatedly in a given unit of work.

  4. They make it possible totransparently batch together multiple database operations.

A persistence context also allows us to detect circularities when performing operations on graphs of entities.(Even in a stateless session, we need some sort of temporary cache of the entity instances we’ve visited while executing a query.)

On the other hand, stateful sessions come with some very important restrictions, since:

  • persistence contexts aren’t threadsafe, and can’t be shared across threads, and

  • a persistence context can’t be reused across unrelated transactions, since that would break the isolation and atomicity of the transactions.

Furthermore, a persistence context holds a hard references to all its entities, preventing them from being garbage collected.Thus, the session must be discarded once a unit of work is complete.

If you don’t completely understand the previous passage, go back and re-read it until you do.A great deal of human suffering has resulted from users mismanaging the lifecycle of the HibernateSession or JPAEntityManager.

We’ll conclude by noting that whether a persistence context helps or harms the performance of a given unit of work depends greatly on the nature of the unit of work.For this reason Hibernate provides both stateful and stateless sessions.

5.2. Creating a session

Sticking with standard JPA-defined APIs, we saw how to obtain anEntityManagerFactory inConfiguration using JPA XML.It’s quite unsurprising that we may use this object to create anEntityManager:

EntityManagerentityManager=entityManagerFactory.createEntityManager();

When we’re finished with theEntityManager, we should explicitly clean it up:

entityManager.close();

On the other hand, if we’re starting from aSessionFactory, as described inProgrammatic configuration using JPA API, we may use:

Sessionsession=sessionFactory.openSession();

But we still need to clean up:

session.close();
Injecting theEntityManager

If you’re writing code for some sort of container environment, you’ll probably obtain theEntityManager by some sort of dependency injection.For example, in Java (or Jakarta) EE you would write:

@PersistenceContextEntityManagerentityManager;

In Quarkus, injection is handled by CDI:

@InjectEntityManagerentityManager;

Outside a container environment, we’ll also have to write code to manage database transactions.

5.3. Managing transactions

Using JPA-standard APIs, theEntityTransaction interface allows us to control database transactions.The idiom we recommend is the following:

EntityManagerentityManager=entityManagerFactory.createEntityManager();EntityTransactiontx=entityManager.getTransaction();try{tx.begin();//do some work...tx.commit();}catch(Exceptione){if(tx.isActive())tx.rollback();throwe;}finally{entityManager.close();}

But this code is extremely tedious, so there’s a cleaner option:

entityManagerFactory.runInTransaction(entityManager->{// do the work...});

When we need to return a value from within the anonymous function, we usecallInTransaction() instead ofrunInTransaction().

Using Hibernate’s native APIs we can write something very similar:

sessionFactory.inTransaction(session->{//do the work...});
Container-managed transactions

In a container environment, the container itself is usually responsible for managing transactions.In Java EE or Quarkus, you’ll probably indicate the boundaries of the transaction using the@Transactional annotation.

TheEntityTransaction interface provides a standard way to set the transaction timeout:

entityManager.getTransaction().setTimeout(30);// 30 seconds

EntityTransaction also provides a way to set the transaction to rollback-only mode:

entityManager.getTransaction().setRollbackOnly();

A transaction in rollback-only mode will be rolled back when it completes.

5.4. Operations on the persistence context

Of course, the main reason we need anEntityManager is to do stuff to the database.The following important operations let us interact with the persistence context and schedule modifications to the data:

Table 37. Methods for modifying data and managing the persistence context
Method name and parametersEffect

persist(Object)

Make a transient object persistent and schedule a SQLinsert statement for later execution

remove(Object)

Make a persistent object transient and schedule a SQLdelete statement for later execution

merge(Object)

Copy the state of a given detached object to a corresponding managed persistent instance and returnthe persistent object

detach(Object)

Disassociate a persistent object from a session withoutaffecting the database

clear()

Empty the persistence context and detach all its entities

flush()

Detect changes made to persistent objects association with the session and synchronize the database state with the state of the session by executing SQLinsert,update, anddelete statements

Notice thatpersist() andremove() have no immediate effect on the database, and instead simply schedule a command for later execution.Also notice that there’s noupdate() operation for a stateful session.Modifications are automatically detected when the session isflushed.

On the other hand, except forgetReference(), the following operations all result in immediate access to the database:

Table 38. Methods for reading and locking data
Method name and parametersEffect

find(Class,Object)

Obtain a persistent object given its type and its id

find(Class,Object,LockModeType)

Obtain a persistent object given its type and its id, requesting the givenoptimistic or pessimistic lock mode

find(EntityGraph,Object)

Obtain a persistent object given its id and anEntityGraph specifying its type and associations which should be eagerly fetched

getReference(Class,id)

Obtain a reference to a persistent object given its type and its id, without actually loading its state from the database

getReference(Object)

Obtain a reference to a persistent object with the same identity as the given detached instance, without actually loading its state from the database

refresh(Object)

Refresh the persistent state of an object using a new SQLselect to retrieve its current state from the database

refresh(Object,LockModeType)

Refresh the persistent state of an object using a new SQLselect to retrieve its current state from the database, requesting the givenoptimistic or pessimistic lock mode

lock(Object,LockModeType)

Obtain anoptimistic or pessimistic lock on a persistent object

Any of these operations might throw an exception.Now, if an exception occurs while interacting with the database, there’s no good way to resynchronize the state of the current persistence context with the state held in database tables.

Therefore, a session is considered to be unusable after any of its methods throws an exception.

The persistence context is fragile.If you receive an exception from Hibernate, you should immediately close and discard the current session. Open a new session if you need to, but throw the bad one away first.

One very important kind of exception which can happen when data is shared between concurrent units of work is anoptimistic lock failure.Optimistic locks are verified by checkingversions.A version check is included in thewhere clause of every SQLupdate ordelete statement for a versioned entity.If a version check fails—​that is, if no rows are updated—​Hibernate infers that the entity was updated in some other unit of work and throws anOptimisticLockException to indicate that the current session is working with stale data.As with other exceptions, this loss of synchronization between the persistence context and the database means that we must discard the current session.

Some of these operations listed above require slightly more care than others.When you calldetach(),clear(),flush(), orrefresh(), you’ve already strayed from the narrow path.You didn’t stray far—​and you probably had a good reason for going there—​but you’re in territory where Hibernate just has to assume you know what you’re doing.If you start to feel that this terrain is bogging you down, consider using astateless session.

Four of these operations acceptoptions, allowing influence over their behavior.

Method name and parametersEffect

find(Class,Object,FindOption…​)

Obtain a persistent object given its type and its id, using the specified options

find(EntityGraph,Object,FindOption…​)

Obtain a persistent object given its id and anEntityGraph specifying its type and associations which should be eagerly fetched, using the specified options

refresh(Object,LockModeType,RefreshOption…​)

Refresh the persistent state of an object using a new SQLselect to retrieve its current state from the database, requesting the givenoptimistic or pessimistic lock mode, using the specified options

lock(Object,LockModeType,LockOption…​)

Obtain anoptimistic or pessimistic lock on a persistent object, using the specified options

For example, JPA provides theTimeout class which is aFindOption, aRefreshOption, and aLockOption.

varbook=entityManger.find(Book.class,isbn,Timeout.ms(100),CacheStoreMode.BYPASS);

Finally, the HibernateSession offers the following method, which is capable of efficiently loading multiple entity instances in parallel:

Method name and parametersEffect

findMultiple(Class,List<Object>,FindOption…​)

Obtain a list of persistent objects given their type and their ids, using the specified options

findMultiple(EntityGraph,List<Object>,FindOption…​)

Obtain a list of persistent objects given their ids and anEntityGraph specifying their type and associations which should be eagerly fetched, using the specified options

The following code results in a single SQLselect statement:

List<Book>books=session.findMultiple(Book.class,bookIds);

Each of the operations we’ve seen so far affects a single entity instance passed as an argument.But there’s a way to set things up so that an operation will propagate to associated entities.

5.5. Cascading persistence operations

It’s quite often the case that the lifecycle of achild entity is completely dependent on the lifecycle of someparent.This is especially common for many-to-one and one-to-one associations, though it’s very rare for many-to-many associations.

For example, it’s quite common to make anOrder and all itsItems persistent in the same transaction, or to delete aProject and itsFiless at once.This sort of relationship is sometimes called awhole/part-type relationship.

Cascading is a convenience which allows us to propagate one of the operations listed inOperations on the persistence context from a parent to its children.To set up cascading, we specify thecascade member of one of the association mapping annotations, usually@OneToMany or@OneToOne.

@EntityclassOrder{...@OneToMany(mappedby=Item_.ORDER,// cascade persist(), remove(), and refresh() from Order to Itemcascade={PERSIST,REMOVE,REFRESH},// also remove() orphaned ItemsorphanRemoval=true)privateSet<Item>items;...}

Orphan removal indicates that anItem should be automatically deleted if it is removed from the set of items belonging to its parentOrder.

5.6. Proxies and lazy fetching

Our data model is a set of interconnected entities, and in Java our whole dataset would be represented as an enormous interconnected graph of objects.It’s possible that this graph is disconnected, but more likely it’s connected, or composed of a relatively small number of connected subgraphs.

Therefore, when we retrieve on object belonging to this graph from the database and instantiate it in memory, we simply can’t recursively retrieve and instantiate all its associated entities.Quite aside from the waste of memory on the VM side, this process would involve a huge number of round trips to the database server, or a massive multidimensional cartesian product of tables, or both.Instead, we’re forced to cut the graph somewhere.

Hibernate solves this problem usingproxies andlazy fetching.A proxy is an object that masquerades as a real entity or collection, but doesn’t actually hold any state, because that state has not yet been fetched from the database.When you call a method of the proxy, Hibernate will detect the call and fetch the state from the database before allowing the invocation to proceed to the real entity object or collection.

Now for the gotchas:

  1. Hibernate will only do this for an entity which is currently associated with a persistence context.Once the session ends, and the persistence context is cleaned up, the proxy is no longer fetchable, and instead its methods throw the hatedLazyInitializationException.

  2. For a polymorphic association, Hibernate does not know the concrete type of the referenced entity when the proxy is instantiated, and so operations likeinstanceof and typecasts do not work correctly when applied to a proxy.

  3. A round trip to the database to fetch the state of a single entity instance is just aboutthe least efficient way to access data.It almost inevitably leads to the infamousN+1 selects problem we’ll discuss later when we talk about how tooptimize association fetching.

The@ConcreteProxy annotation solves gotcha 2, but at the cost of performance (extra joins), and so its use is not generally recommended, except in very special circumstances.

We’re getting a bit ahead of ourselves here, but let’s quickly mention the general strategy we recommend to navigate past these gotchas:

  • All associations should be setfetch=LAZY to avoid fetching extra data when it’s not needed.As we mentionedearlier, this setting is not the default for@ManyToOne associations, and must be specified explicitly.

  • But strive to avoid writing code which triggers lazy fetching.Instead, fetch all the data you’ll need upfront at the beginning of a unit of work, using one of the techniques described inAssociation fetching, usually, usingjoin fetch in HQL or anEntityGraph.

It’s important to know that some operations which may be performed with an unfetched proxydon’t require fetching its state from the database.First, we’re always allowed to obtain its identifier:

varpubId=entityManager.find(Book.class,bookId).getPublisher().getId();// does not fetch publisher

Second, we may create an association to a proxy:

book.setPublisher(entityManager.getReference(Publisher.class,pubId));// does not fetch publisher

Sometimes it’s useful to test whether a proxy or collection has been fetched from the database.JPA lets us do this using thePersistenceUnitUtil:

booleanauthorsFetched=entityManagerFactory.getPersistenceUnitUtil().isLoaded(book.getAuthors());

Hibernate has a slightly easier way to do it:

booleanauthorsFetched=Hibernate.isInitialized(book.getAuthors());

Similarly,PersistenceUnitUtil.load() force-fetches a proxy or collection:

Bookbook=session.find(Book.class,bookId);// fetch just the Book, leaving authors unfetchedentityManagerFactory.getPersistenceUnitUtil().load(book.getAuthors());

Again,Hibernate.initialize() is slightly more convenient:

Bookbook=session.find(Book.class,bookId);// fetch just the Book, leaving authors unfetchedHibernate.initialize(book.getAuthors());// fetch the Authors

On the other hand, the above code is very inefficient, requiring two trips to the database to obtain data that could in principle be retrieved with just one query.

The static methods of theHibernate class let us do a lot more, and it’s worth getting a bit familiar with them.Of particular interest are the operations which let us work with unfetched collections without fetching their state from the database.For example, consider this code:

Bookbook=session.find(Book.class,bookId);// fetch just the Book, leaving authors unfetchedAuthorauthorRef=session.getReference(Author.class,authorId);// obtain an unfetched proxybooleanisByAuthor=Hibernate.contains(book.getAuthors(),authorRef);// no fetching

This code fragment leaves both the setbook.authors and the proxyauthorRef unfetched.

It’s clear from the discussion above that we need a way to request that an association beeagerly fetched using a databasejoin, thus protecting ourselves from the infamous N+1 selects.One way to do this is by passing anEntityGraph tofind().

5.7. Entity graphs and eager fetching

When an association is mappedfetch=LAZY, it won’t, by default, be fetched when we call thefind() method.We may request that an association be fetched eagerly (immediately) by passing anEntityGraph tofind().

vargraph=entityManager.createEntityGraph(Book.class);graph.addSubgraph(Book_.publisher);Bookbook=entityManager.find(graph,bookId);

This code adds aleft outer join to our SQL query, fetching the associatedPublisher along with theBook.

We may even attach additional nodes to ourEntityGraph:

vargraph=session.createEntityGraph(Book.class);graph.addSubgraph(Book_.publisher);graph.addPluralSubgraph(Book_.authors).addSubgraph(Author_.person);Bookbook=entityManager.find(graph,bookId);

This results in a SQL query withfourleft outer joins.

In the code examples above, The classesBook_ andAuthor_ are generated byHibernate Processor, as we saw earlier.They let us refer to attributes of our model in a completely type-safe way.We’ll use them again, below, when we talk aboutCriteria queries.

JPA specifies that any givenEntityGraph may be interpreted in two different ways.

  • Afetch graph specifies exactly the associations that should be eagerly loaded.Any association not belonging to the entity graph is proxied and loaded lazily only if required.

  • Aload graph specifies that the associations in the entity graph are to be fetched in addition to the associations mappedfetch=EAGER.

AnEntityGraph passed directly tofind() is always interpreted as a load graph.

You’re right, the names make no sense.But don’t worry, if you take our advice, and map your associationsfetch=LAZY, there’s no difference between a "fetch" graph and a "load" graph, so the names don’t matter.

JPA even specifies a way to define named entity graphs using annotations.But the annotation-based API is so verbose that it’s just not worth using.

5.8. Controlling lookup by id

As we will soon see in thenext chapter, we can do almost anything viaHQL,criteria, ornative SQL queries.But when we already know the identifier of the entity we need, a query can feel like overkill.And queries don’t make efficient use of thesecond level cache.

We met thefind() andfindMultiple() methodsearlier.These are the most basic ways to perform alookup by id.But they can’t quite do everything.Therefore, Hibernate has some APIs that streamline certain more complicated lookups:

Table 39. Operations for lookup by id
Method namePurpose

byId()

Lets us specify association fetching via anEntityGraph, as we saw; also lets us specify some additional options, including how the lookupinteracts with the second level cache, and whether the entity should be loaded in read-only mode

byMultipleIds()

Lets us load abatch of ids at the same time

Since the introduction ofFindOption in JPA 3.2,byId() is now much less useful.

Batch loading is very useful when we need to retrieve multiple instances of the same entity class by id:

vargraph=session.createEntityGraph(Book.class);graph.addSubgraph(Book_.publisher);List<Book>books=session.byMultipleIds(Book.class).withFetchGraph(graph)// control association fetching.withBatchSize(20)// specify an explicit batch size.with(CacheMode.GET)// control interaction with the cache.multiLoad(bookIds);

The given list ofbookIds will be broken into batches, and each batch will be fetched from the database in a singleselect.If we don’t specify the batch size explicitly, a batch size will be chosen automatically.

We also have some operations for working with lookups bynatural id:

Method namePurpose

bySimpleNaturalId()

For an entity with just one attribute is annotated@NaturalId

byNaturalId()

For an entity with multiple attributes are annotated@NaturalId

byMultipleNaturalId()

Lets us load abatch of natural ids at the same time

Here’s how we can retrieve an entity by its composite natural id:

Bookbook=session.byNaturalId(Book.class).using(Book_.isbn,isbn).using(Book_.printing,printing).load();

Notice that this code fragment is completely typesafe, again thanks toHibernate Processor.

5.9. Flushing the session

From time to time, aflush operation is triggered, and the session synchronizes dirty state held in memory—that is, modifications to the state of entities associated with the persistence context—with persistent state held in the database. Of course, it does this by executing SQLINSERT,UPDATE, andDELETE statements.

By default, a flush is triggered:

  • when the current transaction commits, for example, whenTransaction.commit() is called,

  • before execution of a query whose result would be affected by the synchronization of dirty state held in memory, or

  • when the program directly callsflush().

In the following code, the flush occurs when the transaction commits:

session.getTransaction().begin();session.persist(author);varbooks=// new Author does not affect results of query for Bookssession.createSelectionQuery("from Book")// no need to flush.getResultList();// flush occurs here, just before transaction commitssession.getTransaction().commit();

But in this code, the flush occurs when the query is executed:

session.getTransaction().begin();session.persist(book);varbooks=// new Book would affect results of query for Bookssession.createSelectionQuery("from Book")// flush occurs here, just before query is executed.getResultList();// changes were already flushed to database, nothing to flushsession.getTransaction().commit();

It’s always possible to callflush() explicitly:

session.getTransaction().begin();session.persist(author);session.flush();// explicit flushvarbooks=session.createSelectionQuery("from Book")// nothing to flush.getResultList();// nothing to flushsession.getTransaction().commit();

Notice that SQL statements are not usually executed synchronously by methods of theSession interface likepersist() andremove(). If synchronous execution of SQL is desired, theStatelessSession allows this.

This behavior can be controlled by explicitly setting the flush mode.For example, to disable flushes that occur before query execution, call:

entityManager.setFlushMode(FlushModeType.COMMIT);

Hibernate allows greater control over theflush mode than JPA:

session.setHibernateFlushMode(FlushMode.MANUAL);

Since flushing is a somewhat expensive operation (the session must dirty-check every entity in the persistence context), setting the flush mode toCOMMIT can occasionally be a useful optimization.But take care—​in this mode, queries might return stale data:

session.getTransaction().begin();session.setFlushMode(FlushModeType.COMMIT);// disable AUTO-flushsession.persist(book);varbooks=// flushing on query execution disabledsession.createSelectionQuery("from Book")// no flush, query returns stale results.getResultList();// flush occurs here, just before transaction commitssession.getTransaction().commit();
Table 40. Flush modes
HibernateFlushModeJPAFlushModeTypeInterpretation

MANUAL

Never flush automatically

COMMIT

COMMIT

Flush before transaction commit

AUTO

AUTO

Flush before transaction commit, and before execution of a query whose results might be affected by modifications held in memory

ALWAYS

Flush before transaction commit, and before execution of every query

A second way to reduce the cost of flushing is to load entities inread-only mode:

  • Session.setDefaultReadOnly(true) specifies that all entities loaded by a given session should be loaded in read-only mode by default,

  • SelectionQuery.setReadOnly(true) specifies that every entity returned by a given query should be loaded in read-only mode, and

  • Session.setReadOnly(Object, true) specifies that a given entity already loaded by the session should be switched to read-only mode.

Hibernate’sReadOnlyMode is a customFindOption:

varbook=entityManager.find(Book.class,isbn,ReadOnlyMode.READ_ONLY);

It’s not necessary to dirty-check an entity instance in read-only mode.

5.10. Lifecycle callbacks and entity listeners

The annotations@PrePersist,@PreRemove,@PreUpdate,@PostPersist,@PostRemove,@PostUpdate, and@PostLoad allow an entity to respond to persistence lifecycle operations and maintain its transient internal state.For example:

@EntityclassOrder{...transientdoubletotal;@PostLoadvoidcomputeTotal(){total=items.stream().mapToDouble(i->i.price*i.quantity).sum();}...}

If we need to interact with technical objects, we can place the lifecycle callback on a separate class, called anentity listener.The@EntityListeners annotation specifies the listeners for a given entity class:

@Entity@EntityListeners(OrderEvents.class)classOrder{...}

An entity listener may inject CDI beans:

// entity listener classclassOrderEvents{@InjectEvent<NewOrder>newOrderEvent;@PostPersistvoidnewOrder(Orderorder){// send a CDI eventnewOrderEvent.fire(newNewOrder(order));}}

A single entity listener class may even be a generic listener that receives lifecycle callbacks for multiple different entity classes.

The venerableInterceptor interface is a more powerful alternative to entity listeners.Interceptor and its friendCustomEntityDirtinessStrategy allow advanced users to augment the built-in handling of managed entities with custom behavior.These interfaces are very useful if you’re building your own persistence framework with Hibernate as the foundation.

We’re about to see one wayInterceptor can be used.

5.11. Transient vs detached

Sometimes, Hibernate needs to be able to distinguish whether an entity instance is:

  • a brand-new transient object the client just instantiated usingnew, or

  • a detached object, which previously belonged to a persistence context.

This is a bit of a problem, since there’s no good and efficient way for Hibernate to just tag an entity with a Post-it saying "I’ve seen you before".

Therefore, Hibernate uses heuristics.The two most useful heuristics are:

  1. If the entity has agenerated identifier, the value of the id field is inspected: if the value currently assigned to the id field is the default value for the type of the field, then the object is transient; otherwise, the object is detached.

  2. If the entity has aversion, the value of the version field is inspected: if the value currently assigned to the version field is the default value, or a negative number, then the object is transient; otherwise, the object is detached.

If the entity has neither a generated id, nor a version, Hibernate usually falls back to just doing something reasonable.In extreme cases aSELECT query will be issued to determine whether a matching row exists in the database.

These heuristics aren’t perfect.It’s quite easy to confuse Hibernate by assigning a value to the id field or version field, making a new transient instance look like it’s detached.We therefore strongly discourage assigning values to fields annotated@GeneratedValue or@Version before passing an entity to Hibernate.

If the heuristics ever happen cause a real problem, you may implement your own Post-it tagging viaInterceptor.isTransient().

5.12. Interacting directly with JDBC

From time to time we run into the need to write some code that calls JDBC directly.TheEntityManager now offers a convenient way to do this:

entityManager.runWithConnection((Connectionconnection)->{try(varcallable=connection.prepareCall("{call myproc(?)}")){callable.setLong(1,argument);callable.execute();}});

To return a value, usecallWithConnection() instead ofrunWithConnection().

The HibernateSession has an older, slightly simpler API:

session.doWork(connection->{try(varcallable=connection.prepareCall("{call myproc(?)}")){callable.setLong(1,argument);callable.execute();}});

If the work returns a value, usedoReturningWork() instead ofdoWork().

TheConnection passed to the work is the same connection being used by the session, and so any work performed using that connection occurs in the same transaction context.

In a container environment where transactions and database connections are managed by the container, this might not be the easiest way to obtain the JDBC connection.

5.13. What to do when things go wrong

Object/relational mapping has been called the "Vietnam of computer science".The person who made this analogy is American, and so one supposes that he meant to imply some kind of unwinnable war.This is quite ironic, since at the very moment he made this comment, Hibernate was already on the brink of winning the war.

Today, Vietnam is a peaceful country with exploding per-capita GDP, and ORM is a solved problem.That said, Hibernate is complex, and ORM still presents many pitfalls for the inexperienced, even occasionally for the experienced.Sometimes things go wrong.

In this section we’ll quickly sketch some general strategies for avoiding "quagmires".

  • Understand SQL and the relational model.Know the capabilities of your RDBMS.Work closely with the DBA if you’re lucky enough to have one.Hibernate is not about "transparent persistence" for Java objects.It’s about making two excellent technologies work smoothly together.

  • Log the SQL executed by Hibernate.You cannot know that your persistence logic is correct until you’ve actually inspected the SQL that’s being executed.Even when everything seems to be "working", there might be a lurkingN+1 selects monster.

  • Be careful whenmodifying bidirectional associations.In principle, you should updateboth ends of the association.But Hibernate doesn’t strictly enforce that, since there are situations where such a rule would be too heavy-handed.Whatever the case, it’s up to you to maintain consistency across your model.

  • Neverleak a persistence context across threads or concurrent transactions.Have a strategy or framework to guarantee this never happens.

  • When running queries that return large result sets, take care to consider the size of thesession cache.Consider using astateless session.

  • Think carefully about the semantics of thesecond-level cache, and how the caching policies impact transaction isolation.

  • Avoid fancy bells and whistles you don’t need.Hibernate is incredibly feature-rich, and that’s a good thing, because it serves the needs of a huge number of users, many of whom have one or two very specialized needs.But nobody hasall those specialized needs.In all probability, you have none of them.Write your domain model in the simplest way that’s reasonable, using the simplest mapping strategies that make sense.

  • When something isn’t behaving as you expect,simplify.Isolate the problem.Find the absolute minimum test case which reproduces the behavior,before asking for help online.Most of the time, the mere act of isolating the problem will suggest an obvious solution.

  • If you’re new to Hibernate, avoid frameworks and libraries that "wrap" JPA.You need an excellent understanding of Hibernate and JPAfirst, before introducing unnecessary additional moving parts.If there’s any one criticism of Hibernate and ORM that sometimesdoes ring true, it’s that it takes you too far from direct control over JDBC.An additional layer just takes you even further.If you insist you really do need this extra layer, we beg you to considerHibernate Data Repositories instead of older third-party solutions.

  • Avoid copy/pasting code from random bloggers or stackoverflow reply guys.Many of the suggestions you’ll find online just aren’t the simplest solution, and many aren’t correct for Hibernate 6 and 7.Instead,understand what you’re doing; study the Javadoc of the APIs you’re using; read the JPA specification; follow the advice we give in this document; go direct to the Hibernate team on Zulip.(Sure, we can be a bit cantankerous at times, but wedo always want you to be successful.)

  • Always consider other options.You don’t have to use Hibernate foreverything.

6. Executing queries

Hibernate features three complementary ways to write queries:

  • theHibernate Query Language, an extremely powerful superset of JPQL, which abstracts most of the features of modern dialects of SQL,

  • the JPAcriteria query API, along with extensions, allowing almost any HQL query to be constructed programmatically via a typesafe API, and, of course

  • for when all else fails,native SQL queries.

In addition, Hibernate 7 provides a convenient new way toprogrammatically customize a query before executing it.

6.1. HQL queries

A full discussion of the query language would require almost as much text as the rest of this Short Guide.Fortunately, HQL is already described in exhaustive (and exhausting) detail inA Guide to Hibernate Query Language.It doesn’t make sense to repeat that information here.

Here we want to see how to execute a query via theSession orEntityManager API.The method we call depends on what kind of query it is:

  • selection queries return a result list, but do not modify the data, but

  • mutation queries modify data, and return the number of modified rows.

Selection queries usually start with the keywordselect orfrom, whereas mutation queries begin with the keywordinsert,update, ordelete.

Table 41. Executing HQL
KindSession methodEntityManager methodQuery execution method

Selection

createSelectionQuery(String,Class)

createQuery(String,Class)

getResultList(),getSingleResult(), orgetSingleResultOrNull()

Mutation

createMutationQuery(String)

createQuery(String)

executeUpdate()

So for theSession API we would write:

List<Book>matchingBooks=session.createSelectionQuery("from Book where title like :titleSearchPattern",Book.class).setParameter("titleSearchPattern",titleSearchPattern).getResultList();

Or, if we’re sticking to the JPA-standard APIs:

List<Book>matchingBooks=entityManager.createQuery("select b from Book b where b.title like :titleSearchPattern",Book.class).setParameter("titleSearchPattern",titleSearchPattern).getResultList();

The main difference betweencreateSelectionQuery() andcreateQuery() is thatcreateSelectionQuery() throws an exception if passed a query string that begins withinsert,delete, orupdate.

We’ve been usinggetResultList() because we’ve been expecting our queries to return multiple results.If we’re expecting a query to return a single result, we can usegetSingleResult().

Bookbook=session.createSelectionQuery("from Book where isbn = ?1",Book.class).setParameter(1,isbn).getSingleResult();

Or, if we’re expecting it to return at most one result, we can usegetSingleResultOrNull().

BookbookOrNull=session.createSelectionQuery("from Book where isbn = ?1",Book.class).setParameter(1,isbn).getSingleResultOrNull();

The difference, of course, is thatgetSingleResult() throws an exception if there’s no matching row in the database, whereasgetSingleResultOrNull() just returnsnull.

To execute aMutationQuery, we useexecuteUpdate(), which returns the number of entities affected by theinsert,update, ordelete.

6.2. Query parameters

Queries are often parameterized.

  • In the query above,:titleSearchPattern is called anamed parameter.

  • Alternatively, we may label a parameter by a number. Such a parameter is called anordinal parameter.

We may easily rewrite our query to use an ordinal parameter:

List<Book>matchingBooks=session.createSelectionQuery("from Book where title like ?1",Book.class).setParameter(1,titleSearchPattern).getResultList();

When a query has multiple parameters, named parameters tend to be easier to read, even if slightly more verbose.

Never concatenate user input with HQL and pass the concatenated string tocreateSelectionQuery().This would open up the possibility for an attacker to execute arbitrary code on your database server.

ThesetParameter() methods specify arguments to query parameters.

The two-argument forms ofsetParameter() are perfect for most purposes, butvery occasionally it’s necessary to resolve an ambiguity in the interpretation of the argument value by explicitly specifying thetype of the argument.The best way to identify the type is via a reference to a JPA metamodelType.There are two ways to do this:

For example, we may pass astatic metamodel reference tosetParameter().

session.createSelectionQuery("from Person where address = :address").setParameter("address"address,Person_.address.getType()).getResultList();

6.3. Auto-flush

By default, Hibernate dirty checks entities in the persistence context before executing a query, in order to determine if there are changes which have not yet been flushed to the database, but which might affect the results of the query.If there are unflushed changes, then Hibernate goes ahead and executes an automaticflush before executing the query.That way, the query won’t return stale results which fail to reflect changes made to data within the current unit of work.But if there are many entities association with the persistence context, then this can be an expensive operation.

To disable this behavior, set thequery flush mode toNO_FLUSH:

BookbookOrNull=session.createSelectionQuery("from Book where isbn = ?1",Book.class).setParameter(1,isbn).setQueryFlushMode(QueryFlushMode.NO_FLUSH).getSingleResult();

Or, especially if you’re using JPA-standard APIs, useFlushModeType.COMMIT:

BookbookOrNull=session.createSelectionQuery("from Book where isbn = ?1",Book.class).setParameter(1,isbn).setFlushMode(FlushModeType.COMMIT).getSingleResult();

Setting the flush mode toNO_FLUSH,COMMIT, orMANUAL might cause the query to return stale results.

Occasionally we need to build a query at runtime, from a set of optional conditions.For this, JPA offers an API which allows programmatic construction of a query.

6.4. Criteria queries

Imagine we’re implementing some sort of search screen, where the user of our system is offered several different ways to constrain the query result set.For example, we might let them search for books by title and/or the author name.Of course, we could construct a HQL query by string concatenation, but this is a bit fragile, so it’s quite nice to have an alternative.

HQL is implemented in terms of criteria objects

Actually, since Hibernate 6, every HQL query is compiled to a criteria query before being translated to SQL.This ensures that the semantics of HQL and criteria queries are identical.

First we need an object for building criteria queries.Using the JPA-standard APIs, this would be aCriteriaBuilder, and we get it from theEntityManagerFactory:

CriteriaBuilderbuilder=entityManagerFactory.getCriteriaBuilder();

But if we have aSessionFactory, we get something much better, aHibernateCriteriaBuilder:

HibernateCriteriaBuilderbuilder=sessionFactory.getCriteriaBuilder();

TheHibernateCriteriaBuilder extendsCriteriaBuilder and adds many operations that JPQL doesn’t have.

If you’re usingEntityManagerFactory, don’t despair, you have two perfectly good ways to obtain theHibernateCriteriaBuilder associated with that factory.Either:

HibernateCriteriaBuilderbuilder=entityManagerFactory.unwrap(SessionFactory.class).getCriteriaBuilder();

Or simply:

HibernateCriteriaBuilderbuilder=(HibernateCriteriaBuilder)entityManagerFactory.getCriteriaBuilder();

We’re ready to create a criteria query.

CriteriaQuery<Book>query=builder.createQuery(Book.class);Root<Book>book=query.from(Book.class);Predicatewhere=builder.conjunction();if(titlePattern!=null){where=builder.and(where,builder.like(book.get(Book_.title),titlePattern));}if(namePattern!=null){Join<Book,Author>author=book.join(Book_.author);where=builder.and(where,builder.like(author.get(Author_.name),namePattern));}query.select(book).where(where).orderBy(builder.asc(book.get(Book_.title)));

Here, as before, the classesBook_ andAuthor_ are generated byHibernate Processor.

Notice that we didn’t bother treatingtitlePattern andnamePattern as parameters.That’s safe because, by default, Hibernate automatically and transparently treats strings passed to theCriteriaBuilder as JDBC parameters.

Execution of a criteria query works almost exactly like execution of HQL.

Table 42. Executing criteria queries
KindSession methodEntityManager methodQuery execution method

Selection

createSelectionQuery(CriteriaQuery)

createQuery(CriteriaQuery)

getResultList(),getSingleResult(), orgetSingleResultOrNull()

Mutation

createMutationQuery(CriteriaUpdate) orcreateMutationQuery(CriteriaDelete)

createQuery(CriteriaUpdate) orcreateQuery(CriteriaDelete)

executeUpdate()

For example:

List<Book>matchingBooks=session.createSelectionQuery(query).getResultList();

Update, insert, and delete queries work similarly:

CriteriaDelete<Book>delete=builder.createCriteriaDelete(Book.class);Root<Book>book=delete.from(Book.class);delete.where(builder.lt(builder.year(book.get(Book_.publicationDate)),2000));session.createMutationQuery(delete).executeUpdate();

It’s even possible to transform a HQL query string to a criteria query, and modify the query programmatically before execution:

HibernateCriteriaBuilderbuilder=sessionFactory.getCriteriaBuilder();varquery=builder.createQuery("from Book where year(publicationDate) > 2000",Book.class);varroot=query.getRoot(0,Book.class);query.where(builder.like(root.get(Book_.title),builder.literal("Hibernate%")));query.orderBy(builder.asc(root.get(Book_.title)),builder.desc(root.get(Book_.isbn)));List<Book>matchingBooks=session.createSelectionQuery(query).getResultList();

This is starting to get a bit messy.In Hibernate 7, we can often useRestriction instead.

Do you find some of the code above a bit too verbose?We do.

6.5. A more comfortable way to write criteria queries

Actually, what makes the JPA criteria API less ergonomic than it should be is the need to call all operations of theCriteriaBuilder as instance methods, instead of having them asstatic functions.The reason it works this way is that each JPA provider has its own implementation ofCriteriaBuilder.

The helper classCriteriaDefinition can reduce the verbosity of criteria queries by eliminating the need to explicitly qualify calls to the methods ofCriteriaBuilder.Ourprevious example would look like this:

CriteriaQuery<Book>query=newCriteriaDefinition(entityManagerFactory,Book.class){{select(book);if(titlePattern!=null){restrict(like(book.get(Book_.title),titlePattern));}if(namePattern!=null){varauthor=book.join(Book_.author);restrict(like(author.get(Author_.name),namePattern));}orderBy(asc(book.get(Book_.title)));}};

When all else fails, and sometimes even before that, we’re left with the option of writing a query in SQL.

6.6. Native SQL queries

HQL is a powerful language which helps reduce the verbosity of SQL, and significantly increases portability of queries between databases.But ultimately, the true value of ORM is not in avoiding SQL, but in alleviating the pain involved in dealing with SQL result sets once we get them back to our Java program.As we saidright up front, Hibernate’s generated SQL is meant to be used in conjunction with handwritten SQL, and native SQL queries are one of the facilities we provide to make that easy.

Table 43. Executing SQL
KindSession methodEntityManager methodQuery execution method

Selection

createNativeQuery(String,Class)

createNativeQuery(String,Class)

getResultList(),getSingleResult(), orgetSingleResultOrNull()

Mutation

createNativeMutationQuery(String)

createNativeQuery(String)

executeUpdate()

Stored procedure

createStoredProcedureCall(String)

createStoredProcedureQuery(String)

execute()

For the most simple cases, Hibernate can infer the shape of the result set:

Bookbook=session.createNativeQuery("select * from Books where isbn = ?1",Book.class).setParameter(1,isbn).getSingleResult();Stringtitle=session.createNativeQuery("select title from Books where isbn = ?1",String.class).setParameter(1,isbn).getSingleResult();

However, in general, there isn’t enough information in the JDBCResultSetMetaData to infer the mapping of columns to entity objects.So for more complicated cases, you’ll need to use the@SqlResultSetMapping annotation to define a named mapping, and pass the name tocreateNativeQuery(). This gets fairly messy, so we don’t want to hurt your eyes by showing you an example of it.

By default, Hibernate doesn’t flush the session before execution of a native query.That’s because the session is unaware of which modifications held in memory would affect the results of the query.

So if there are any unflushed changes toBooks, this query might return stale data:

List<Book>books=session.createNativeQuery("select * from Books",Book.class).getResultList();

There’s two ways to ensure the persistence context is flushed before this query is executed.

Either, we could simply force a flush by callingflush() or by setting the flush mode toALWAYS:

List<Book>books=session.createNativeQuery("select * from Books",Book.class).setHibernateFlushMode(ALWAYS).getResultList();

Or, alternatively, we could tell Hibernate which modified state affects the results of the query:

List<Book>books=session.createNativeQuery("select * from Books",Book.class).addSynchronizedEntityClass(Book.class).getResultList();

You can call stored procedures usingcreateStoredProcedureQuery() orcreateStoredProcedureCall().

6.7. Restrictions and ordering

We’ve already seen how the JPACriteria Query API can be used to construct a query completely programmatically.The Criteria API is powerful, but for the most common scenarios it’s at least arguably overkill.TheCriteriaDefinition class helps a bit, but it doesn’t completely eliminate the verbosity of programmatic query definition.

In Hibernate 7, there’s a new option, a very ergonomic API for programmatically adding restrictions or ordering to an existing query before executing it.(Actually, the ordering part of this was introduced in Hibernate 6.5.)This new API:

  • isn’t part of the Criteria Query API, and so we don’t need aCriteriaQuery object to make use of it,

  • does make use of the JPAstatic metamodel for type safety,

  • works with both HQL and Criteria queries, and

  • is optimized for the case of a query which returns its single root entity.

varselection=SelectionSpecification.create(Book.class,// an optional base query, written in HQL:"from Book where year(publicationDate) > 2000");// add programmatic restrictions:if(titlePattern!=null)selection.restrict(Restriction.like(Book_.title,namePattern));if(isbns!=null&&!isbns.isEmpty())selection.restrict(Restriction.in(Book_.isbn,isbns));// add programmatic ordering:if(orderByTitle)selection.sort(Order.asc(Book_.title));if(orderByIsbn)selection.sort(Order.asc(Book_.isbn));// add programmatic association fetching:if(fetchPublisher)selection.fetch(Path.from(Book.class).to(Book_.publisher));// execute the query in the given session:List<Book>matchingBooks=selection.createQuery(session).getResultList();

Notice that:

  • TheRestriction interface has static methods for constructing a variety of different kinds of restriction in a completely typesafe way.

  • Similarly, theOrder interface has a variety of static methods for constructing different kinds of sorting criteria.

We need the following methods ofSelectionSpecification:

Table 44. Methods for query restriction and ordering
Method namePurpose

restrict()

Add aRestriction on the query results

sort(),resort()

Specify how the query results should be ordered

fetch()

Add a fetched associationPath

augment()

Add a custom function which directly manipulates the select query

Two of these operations are also available for aMutationSpecification:

Table 45. Methods for mutation restriction
Method namePurpose

restrict()

Add aRestriction on the records to be updated

augment()

Add a custom function which directly manipulates the update or delete query

Alternatively,Restriction andOrder can be used withgenerated query or finder methods, and even withJakarta Data repositories.

The interfacePath may be used to express restrictions on fields of an embedded or associated entity class.It may even be used for association fetching.

List<Book>booksForPublisher=SelectionSpecification.create(Book.class).restrict(Path.from(Book.class).to(Book_.publisher).to(Publisher_.name).equalTo(publisherName)).fetch(Path.from(Book.class).to(Book_.publisher)).createQuery(session).getResultList();

Specifications aren’t for everything, however.

SelectionSpecification (similar to its friendMutationSpecification) may be used in cases where a query returns a single "root" entity, possibly with some fetched associations.It’s not useful in cases where a query should return multiple entities, a projection of entity fields, or an aggregation.For such cases, the full Criteria API is appropriate.

Finally, theaugment() method deserves its own subsection.

6.8. Augmentation

WhenRestriction,Path, andOrder aren’t expressive enough, we canaugment the query by manipulating its representation as a criteria:

varbooks=SelectionSpecification.create(Book.class).augment((builder,query,book)->// augment the query via JPA Criteria APIquery.where(builder.like(book.get(Book_.title),titlePattern))).orderBy(builder.asc(book.get(Book_.isbn))).createQuery(session).getResultList();

For really advanced cases,augment() works quite nicely withCriteriaDefinition.

varbooks=SelectionSpecification.create(Book.class).augment((builder,query,book)->// eliminate explicit references to 'builder'newCriteriaDefinition<>(query){{where(like(entity.get(BasicEntity_.title),titlePattern),greaterThan(book.get(Book_.pages),minPages));orderBy(asc(book.get(Book_.isbn)));}}).createQuery(session).getResultList();

However, we emphasize that this API shines in cases where complex manipulations arenot required.For complicated queries involving multiple entities, or with aggregation and projection, you’re best off heading straight to theCriteriaBuilder.

Programmatic restrictions, and especially programmatic ordering, are often used together with pagination.

6.9. Limits and pagination

If a query might return more results than we can handle at one time, we may specify:

  • alimit on the maximum number of rows returned, and,

  • optionally, anoffset, the first row of an ordered result set to return.

The offset is used to paginate query results.

There’s two ways to add a limit or offset to a HQL or native SQL query:

  • using the syntax of the query language itself, for example,offset 10 rows fetch next 20 rows only, or

  • using the methodssetFirstResult() andsetMaxResults() of theSelectionQuery interface.

If the limit or offset is parameterized, the second option is simpler.For example, this:

List<Book>books=session.createSelectionQuery("from Book where title like ?1 order by title",Book.class).setParameter(1,titlePattern).setMaxResults(MAX_RESULTS).getResultList();

is simpler than:

// a worse way to do paginationList<Book>books=session.createSelectionQuery("from Book where title like ?1 order by title fetch first ?2 rows only",Book.class).setParameter(1,titlePattern).setParameter(2,MAX_RESULTS).getResultList();

Hibernate’sSelectionQuery has a slightly different way to paginate the query results:

List<Book>books=session.createSelectionQuery("from Book where title like ?1 order by title",Book.class).setParameter(1,titlePattern).setPage(Page.first(MAX_RESULTS)).getResultList();

ThegetResultCount() method is useful for displaying the number of pages of results:

SelectionQuery<Book>query=session.createSelectionQuery("from Book where title like ?1 order by title",Book.class).setParameter(1,titlePattern);longresults=query.getResultCount();longpages=results/MAX_RESULTS+(results%MAX_RESULTS==0?0:1);List<Book>books=query.setMaxResults(MAX_RESULTS).getResultList();
Table 46. Methods for query limits, pagination, and ordering
Method namePurposeJPA-standard

setMaxResults()

Set a limit on the number of results returned by a query

setFirstResult()

Set an offset on the results returned by a query

setPage()

Set the limit and offset by specifying aPage object

getResultCount()

Determine how many results the query would return in the absence of any limit or offset

It’s quite common for pagination to be combined with the need to order query results by a field that’s determined at runtime.TheOrder class we just metabove provides the ability to specify that the query results should be ordered by one or more fields of the entity type returned by the query:

List<Book>books=session.createSelectionQuery("from Book where title like ?1",Book.class).setParameter(1,titlePattern).setOrder(List.of(Order.asc(Book_.title),Order.asc(Book_.isbn))).setMaxResults(MAX_RESULTS).getResultList();

The approach to pagination we’ve just seen is sometimes calledoffset-based pagination.Since Hibernate 6.5, there’s an alternative approach, which offers some advantages, though it’s a little more difficult to use.

6.10. Key-based pagination

Key-based pagination aims to reduce the likelihood of missed or duplicate results when data is modified between page requests.It’s most easily illustrated with an example:

StringQUERY="from Book where publicationDate > :minDate";// obtain the first page of resultsKeyedResultList<Book>first=session.createSelectionQuery(QUERY,Book.class).setParameter("minDate",minDate).getKeyedResultList(Page.first(25).keyedBy(Order.asc(Book_.isbn)));List<Book>firstPage=first.getResultList();...if(!firstPage.isLastPage()){// obtain the second page of resultsKeyedResultList<Book>second=session.createSelectionQuery(QUERY,Book.class).setParameter("minDate",minDate)).getKeyedResultList(firstPage.getNextPage());List<Book>secondPage=second.getResultList();...}

The "key" in key-based pagination refers to a unique key of the result set which determines a total order on the query results.In this example,Book.isbn is the key.

Since this code is a little bit fiddly, key-based pagination works best withgenerated query or finder methods.

6.11. Representing projection lists

Aprojection list is the list of things that a query returns, that is, the list of expressions in theselect clause.Since Java has no tuple types, representing query projection lists in Java has always been a problem for JPA and Hibernate.Traditionally, we’ve just usedObject[] most of the time:

varresults=session.createSelectionQuery("select isbn, title from Book",Object[].class).getResultList();for(varresult:results){varisbn=(String)result[0];vartitle=(String)result[1];...}

This is really a bit ugly.Java’srecord types now offer an interesting alternative:

recordIsbnTitle(Stringisbn,Stringtitle){}varresults=session.createSelectionQuery("select isbn, title from Book",IsbnTitle.class).getResultList();for(varresult:results){varisbn=result.isbn();vartitle=result.title();...}

Notice that we’re able to declare therecord right before the line which executes the query.

This works just as well with queries written in SQL:

recordBookInfo(Stringisbn,Stringtitle,intpages){}List<BookInfo>resultList=session.createNativeQuery("select title, isbn, pages from Book",BookInfo.class).getResultList();

Now, this approach is onlysuperficially more typesafe, since the query itself is not checked statically, and so we can’t say it’s objectively better.But perhaps you find it more aesthetically pleasing.And if we’re going to be passing query results around the system, the use of arecord type ismuch better.

The criteria query API offers a much more satisfying solution to the problem.Consider the following code:

varbuilder=sessionFactory.getCriteriaBuilder();varquery=builder.createTupleQuery();varbook=query.from(Book.class);varbookTitle=book.get(Book_.title);varbookIsbn=book.get(Book_.isbn);varbookPrice=book.get(Book_.price);query.select(builder.tuple(bookTitle,bookIsbn,bookPrice));varresultList=session.createSelectionQuery(query).getResultList();for(varresult:resultList){Stringtitle=result.get(bookTitle);Stringisbn=result.get(bookIsbn);BigDecimalprice=result.get(bookPrice);...}

This code is manifestly completely typesafe, and much better than we can hope to do with HQL.

6.12. Named queries

The@NamedQuery annotation lets us define a HQL query that is compiled and checked as part of the bootstrap process.This means we find out about errors in our queries earlier, instead of waiting until the query is actually executed.We can place the@NamedQuery annotation on any class, even on an entity class.

@NamedQuery(name="10BooksByTitle",query="from Book where title like :titlePattern order by title fetch first 10 rows only")classBookQueries{}

We have to make sure that the class with the@NamedQuery annotation will be scanned by Hibernate, either:

  • by adding<class>org.hibernate.example.BookQueries</class> topersistence.xml, or

  • by callingpersistenceConfiguration.managedClass(BookQueries.class).

Unfortunately, JPA’s@NamedQuery annotation can’t be placed on a package descriptor.Therefore, Hibernate provides a very similar annotation,@org.hibernate.annotations.NamedQuery whichcan be specified at the package level.If we declare a named query at the package level, we must call:

configuration.addPackage("org.hibernate.example")

so that Hibernate knows where to find it.

The@NamedNativeQuery annotation lets us do the same for native SQL queries.There’s much less advantage to using@NamedNativeQuery, because there is very little that Hibernate can do to validate the correctness of a query written in the native SQL dialect of your database.

Table 47. Executing named queries
KindSession methodEntityManager methodQuery execution method

Selection

createNamedSelectionQuery(String,Class)

createNamedQuery(TypedQueryReference),createNamedQuery(String,Class)

getResultList(),getSingleResult(),getSingleResultOrNull()

Mutation

createNamedMutationQuery(String)

createNamedQuery(TypedQueryReference),createNamedQuery(String)

executeUpdate()

We execute our named query like this:

List<Book>books=entityManager.createQuery(BookQueries_._10BooksByTitle_).setParameter("titlePattern",titlePattern).getResultList()

Here,BookQueries_._10BooksByTitle_ is an element of the JPA static metamodel of typeTypedQueryReference<Book>, generated by Hibernate Processor.

Note that the code which executes the named query is not aware of whether the query was written in HQL or in native SQL, making it slightly easier to change and optimize the query later.

It’s nice to have our queries checked at startup time.It’s even better to have them checked at compile time.InOrganizing persistence logic, we mentioned that the Hibernate Processor can do that for us, with the help of the@CheckHQL annotation, and we presented that as a reason to use@NamedQuery.

Actually, Hibernate even has a separateQuery Validator capable of performing compile-time validation of HQL query strings that occur as arguments tocreateQuery() and friends.If we use the Query Validator, there’s not much advantage to the use of named queries.

We’re going to learn more about Hibernate Processor in the next chapter.

7. Compile-time tooling

Thestatic metamodel generator is a standard part of JPA.We’ve actually already seen its handiwork in the code examplesearlier: it’s the author of the classBook_, which contains the static metamodel of theentity classBook.

Hibernate comes with an annotation processor which does much more than just this.It’s capable of automatically generating:

Hibernate Processor

Hibernate Processor, the annotation processor formerly known as the Metamodel Generator, began its life as a code generator for what JPA calls astatic metamodel.That is, it produces a typed model of the persistent classes in our program, giving us a type safe way to refer to their attributes in Java code.In particular, it lets us specifyentity graphs andcriteria queries in a completely type-safe way.

The history behind this thing is quite interesting.Back when Java’s annotation processing API was brand spankin' new, the static metamodel for JPA was proposed by Gavin King for inclusion in JPA 2.0, as a way to achieve type safety in the nascent criteria query API.It’s fair to say that, back in 2010, this API was not a runaway success.Tools did not, at the time, feature robust support for annotation processors.And all the explicit generic types made user code quite verbose and difficult to read.(The need for an explicit reference to aCriteriaBuilder instance also contributed verbosity to the criteria API.)For years, Gavin counted this as one of his more embarrassing missteps.

But time has been kind to the static metamodel.By now, all Java compilers, build tools, and IDEs have robust support for annotation processing, and Java’s local type inference (thevar keyword) eliminates the verbose generic types.JPA’sCriteriaBuilder andEntityGraph APIs are still not quite perfect, but the imperfections aren’t related to static type safety or annotation processing.The static metamodel itself is undeniably useful and elegant.

And it turns out that there was quite a lot of unlocked potential there.Since Hibernate 6.3 the Processor has started taking on a much bigger role.Today, it even contains acomplete implementation of the Jakarta Data specification.

Now, you still don’t have to use the Hibernate Processor with Hibernate—the APIs we just mentioned still also accept plain strings—but we find that it works well with Gradle and integrates smoothly with our IDE, and the advantage in type-safety is compelling.

We’ve already seen how to set up the annotation processor in theGradle build we saw earlier.For more details on how to integrate the Hibernate Processor, check out theStatic Metamodel Generator section in the User Guide.

7.1. The static metamodel

We’ve already seen several ways to use the JPA static metamodel.Metamodel references are useful for expressing, in a completely type-safe way:

Here’s an example of the sort of code that’s generated for an entity class, as mandated by the JPA specification:

Generated Code
@StaticMetamodel(Book.class)publicabstractclassBook_{/**     * @see org.example.Book#isbn     **/publicstaticvolatileSingularAttribute<Book,String>isbn;/**     * @see org.example.Book#text     **/publicstaticvolatileSingularAttribute<Book,String>text;/**     * @see org.example.Book#title     **/publicstaticvolatileSingularAttribute<Book,String>title;/**     * @see org.example.Book#type     **/publicstaticvolatileSingularAttribute<Book,Type>type;/**     * @see org.example.Book#publicationDate     **/publicstaticvolatileSingularAttribute<Book,LocalDate>publicationDate;/**     * @see org.example.Book#publisher     **/publicstaticvolatileSingularAttribute<Book,Publisher>publisher;/**     * @see org.example.Book#authors     **/publicstaticvolatileSetAttribute<Book,Author>authors;publicstaticfinalStringISBN="isbn";publicstaticfinalStringTEXT="text";publicstaticfinalStringTITLE="title";publicstaticfinalStringTYPE="type";publicstaticfinalStringPUBLICATION_DATE="publicationDate";publicstaticfinalStringPUBLISHER="publisher";publicstaticfinalStringAUTHORS="authors";}

For each attribute of the entity, theBook_ class has:

  1. aString-valued constant likeTITLE , and

  2. a typesafe reference liketitle to a metamodel object of typeAttribute.

Hibernate Processor allowsstatically-typed access to elements of the JPAMetamodel. But theMetamodel is also accessible in a "reflective" way, via theEntityManagerFactory.

EntityType<Book>book=entityManagerFactory.getMetamodel().entity(Book.class);SingularAttribute<Book,Long>id=book.getDeclaredId(Long.class)

This is very useful for writing generic code in frameworks or libraries.For example, you could use it to create your own criteria query API.

The JPA static metamodel for an entity also contains members representing the named queries and named entity graphs declared by@NamedQuery,@NamedNativeQuery, and@NamedEntityGraph annotations of the entity class.

For example, if we had:

@CheckHQL// validate named queries at compile time@NamedQuery(name="findBooksByTitle",query="from Book where title like :title order by title")@EntityclassBook{...}

Then we may execute the query as follows:

varbooks=entityManager.createNamedQuery(Queries_._findBooksByTitle_).setParameter("title",titlePattern).setPage(page).getResultList();

Notice that no typecast was required here, since the generated code embeds the return type of the query as a type argument of the JPATypedQueryReference:

/** * @see #_findBooksByTitle_ **/publicstaticfinalStringQUERY_FIND_BOOKS_BY_TITLE="findBooksByTitle";/** * The query named {@value QUERY_FIND_BOOKS_BY_TITLE} * <pre> * from Book where title like :title order by title * </pre> * * @see org.example.Book **/publicstaticvolatileTypedQueryReference<Book>_findBooksByTitle_;

Actually, Hibernate Processor doesn’t require that such annotations be applied to the entity class itself, as wealready saw earlier.

We’ve already been using metamodel references likeBook_.authors andBook.AUTHORS in the previous chapters.So now let’s see what else Hibernate Processor can do for us.

7.2. Finder methods, query methods, and repositories

Automatic generation offinder methods andquery methods is a relatively new feature of Hibernate Processor—​originally introduced as an experiment—​which ultimately grew into a whole new way to use Hibernate.

We’re going to meet three different kinds of generated method:

  • anamed query method has its signature and implementation generated directly from a@NamedQuery annotation,

  • aquery method has a signature that’s explicitly declared, and a generated implementation which executes a HQL or SQL query specified via a@HQL or@SQL annotation, and

  • afinder method annotated@Find has a signature that’s explicitly declared, and a generated implementation inferred from the parameter list.

We’re also going to see two ways that these methods can be called:

Back inOrganizing persistence logic, we walked you through a few different ways to organize your code with the help of Hibernate Processor.That journey terminated at the idea of a repository, but we emphasized that you aren’t required to stay all the way to the end of the line.Repositories are a sweet spot for many users, but they might not be your sweet spot, and that’s OK.Hibernate Processor is perfectly happy to generatestatic implementations of@HQL,@SQL, and@Find methods, eliminating the need to inject or instantiate a repository object.

Hibernate Processor and Jakarta Data

The functionality we’re about to describe was developed before Jakarta Data took on its current shape, and directly triggered the apocalypse which lead to the final form of the specification.Therefore, there’s massive overlap between the functionality described in this chapter, and the functionality available via the Jakarta Data annotations.On the other hand, Jakarta Data can’t doeverything described below, and in particular it doesn’t yet come with built-in support for stateful persistence contexts or reactive sessions.

We’ve therefore optednot to rewrite this chapter in a Jakarta Data-centric way, and instead refer you toIntroducing Hibernate Data Repositories for information about the standard Jakarta Data APIs.

As Jakarta Data matures, even more of this functionality might be made obsolete, at least in the form described here.We’re working hard to make that happen.

The functionality described in the rest of this chapter depends on the use of the annotations described inEntities.Hibernate Processor is not currently able to generate finder methods and query methods for entities declared completely in XML, and it’s not able to validate HQL which queries such entities.(On the other hand, theO/R mappings may be specified in XML, since they’re not needed by the Processor.)

To whet our appetites, let’s see how it works for a@NamedQuery.

7.3. Named queries and Hibernate Processor

The very simplest way to generate a query method is to put a@NamedQuery annotation anywhere we like, with aname beginning with the magical character#.

Let’s just stick it on theBook class:

@CheckHQL// validate the query at compile time@NamedQuery(name="#findByTitleAndType",query="select book from Book book where book.title like :title and book.type = :type")@EntitypublicclassBook{...}

Now the Processor adds the following method declaration to the metamodel classBook_.

Generated Code
/** * Execute named query {@value #QUERY_FIND_BY_TITLE_AND_TYPE} defined by annotation of {@link Book}. **/publicstaticList<Book>findByTitleAndType(@NonnullEntityManagerentityManager,Stringtitle,Typetype){returnentityManager.createNamedQuery(QUERY_FIND_BY_TITLE_AND_TYPE).setParameter("title",title).setParameter("type",type).getResultList();}

We can easily call this method from wherever we like, as long as we have access to anEntityManager:

List<Book>books=Book_.findByTitleAndType(entityManager,titlePattern,Type.BOOK);

Now, this is quite nice, but it’s a bit inflexible in various ways, and so this probablyisn’t the best way to generate a query method.

7.4. Generated query methods

The principal problem with generating the query method straight from the@NamedQuery annotation is that it doesn’t let us explicitly specify the return type or parameter list.In the case we just saw, Hibernate Processor does a reasonable job of inferring the query return type and parameter types, but we’re often going to need a bit more control.

The solution is to write down the signature of the query methodexplicitly, as an abstract method in Java.We’ll need a place to put this method, and since ourBook entity isn’t an abstract class, we’ll just introduce a new interface for this purpose:

interfaceQueries{@HQL("where title like :title and type = :type")List<Book>findBooksByTitleAndType(Stringtitle,Stringtype);}

Instead of@NamedQuery, which is a type-level annotation, we specify the HQL query using the new@HQL annotation, which we place directly on the query method.This results in the following generated code in theQueries_ class:

Generated Code
@StaticMetamodel(Queries.class)publicabstractclassQueries_{/**     * Execute the query {@value #FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type}.     *     * @see org.example.Queries#findBooksByTitleAndType(String,Type)     **/publicstaticList<Book>findBooksByTitleAndType(@NonnullEntityManagerentityManager,Stringtitle,Typetype){returnentityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type,Book.class).setParameter("title",title).setParameter("type",type).getResultList();}staticfinalStringFIND_BOOKS_BY_TITLE_AND_TYPE_String_Type="where title like :title and type = :type";}

Notice that the signature differs just slightly from the one we wrote down in theQueries interface: the Processor has prepended a parameter acceptingEntityManager to the parameter list.

If we want to explicitly specify the name and type of this parameter, we may declare it explicitly:

interfaceQueries{@HQL("where title like :title and type = :type")List<Book>findBooksByTitleAndType(StatelessSessionsession,Stringtitle,Stringtype);}

Hibernate Processor defaults to usingEntityManager as the session type, but other types are allowed:

  • Session,

  • StatelessSession, or

  • Mutiny.Session orMutiny.StatelessSession from Hibernate Reactive.

The real value of all this is in the checks which can now be done at compile time.Hibernate Processor verifies that the parameters of our abstract method declaration match the parameters of the HQL query, for example:

  • for a named parameter:alice, there must be a method parameter namedalice with exactly the same type, or

  • for an ordinal parameter?2, the second method parameter must have exactly the same type.

The query must also be syntactically legal and semantically well-typed, that is, the entities, attributes, and functions referenced in the query must actually exist and have compatible types.Hibernate Processor determines this by inspecting the annotations of the entity classes at compile time.

The@CheckHQL annotation which instructs Hibernate to validate named queries isnot necessary for query methods annotated@HQL.

The@HQL annotation has a friend named@SQL which lets us specify a query written in native SQL instead of in HQL.In this case there’s a lot less the Processor can do to check that the query is legal and well-typed.

We imagine you’re wondering whether astatic method is really the right thing to use here.

7.5. Generating query methods as instance methods

One thing not to like about what we’ve just seen is that we can’t transparently replace a generatedstatic function of theQueries_ class with an improved handwritten implementation without impacting clients.Now, if our query is only called in one place, which is quite common, this isn’t going to be a big issue, and so we’re inclined to think thestatic function is fine.

But if this function is called from many places, it’s probably better to promote it to an instance method of some class or interface.Fortunately, this is straightforward.

All we need to do is add an abstract getter method for the session object to ourQueries interface.(And remove the session from the method parameter list.)We may call this method anything we like:

interfaceQueries{EntityManagerentityManager();@HQL("where title like :title and type = :type")List<Book>findBooksByTitleAndType(Stringtitle,Stringtype);}

Here we’ve usedEntityManager as the session type, but other types are allowed, as we saw above.

Now Hibernate Processor does something a bit different:

Generated Code
@StaticMetamodel(Queries.class)publicclassQueries_implementsQueries{privatefinal@NonnullEntityManagerentityManager;publicQueries_(@NonnullEntityManagerentityManager){this.entityManager=entityManager;}public@NonnullEntityManagerentityManager(){returnentityManager;}/**     * Execute the query {@value #FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type}.     *     * @see org.example.Queries#findBooksByTitleAndType(String,Type)     **/@OverridepublicList<Book>findBooksByTitleAndType(Stringtitle,Typetype){returnentityManager.createQuery(FIND_BOOKS_BY_TITLE_AND_TYPE_String_Type,Book.class).setParameter("title",title).setParameter("type",type).getResultList();}staticfinalStringFIND_BOOKS_BY_TITLE_AND_TYPE_String_Type="where title like :title and type = :type";}

The generated classQueries_ now implements theQueries interface, and the generated query method implements our abstract method directly.

Of course, the protocol for calling the query method has to change:

Queriesqueries=newQueries_(entityManager);List<Book>books=queries.findByTitleAndType(titlePattern,Type.BOOK);

If we ever need to swap out the generated query method with one we write by hand, without impacting clients, all we need to do is replace the abstract method with adefault method of theQueries interface.For example:

interfaceQueries{EntityManagerentityManager();// handwritten method replacing previous generated implementationdefaultList<Book>findBooksByTitleAndType(Stringtitle,Stringtype){entityManager().createQuery("where title like :title and type = :type",Book.class).setParameter("title",title).setParameter("type",type).setFlushMode(COMMIT).setMaxResults(100).getResultList();}}

What if we would like to inject aQueries object instead of calling its constructor directly?

As yourecall, we don’t think these things really need to be container-managed objects.But if youwant them to be—​if you’re allergic to calling constructors, for some reason—​then:

  • placingjakarta.inject on the build path will cause an@Inject annotation to be added to the constructor ofQueries_, and

  • placingjakarta.enterprise.context on the build path will cause a@Dependent annotation to be added to theQueries_ class.

Thus, the generated implementation ofQueries will be a perfectly functional CDI bean with no extra work to be done.

Is theQueries interface starting to look a lot like a DAO-style repository object?Well, perhaps.You can certainlydecide to use this facility to create aBookRepository if that’s what you prefer.But unlike a repository, ourQueries interface:

  • doesn’t attempt to hide theEntityManager from its clients,

  • doesn’t implement or extend any framework-provided interface or abstract class, at least not unless you want to create such a framework yourself, and

  • isn’t restricted to service a particular entity class.

We can have as many or as few interfaces with query methods as we like.There’s no one-one-correspondence between these interfaces and entity types.This approach is so flexible that we don’t even really know what to call these "interfaces with query methods".

7.6. Generated finder methods

At this point, one usually begins to question whether it’s even necessary to write a query at all.Would it be possible to just infer the query from the method signature?

In some simple cases it’s indeed possible, and this is the purpose offinder methods.A finder method is a method annotated@Find.For example:

@FindBookgetBook(Stringisbn);

A finder method may have multiple parameters:

@FindList<Book>getBooksByTitle(Stringtitle,Typetype);

The name of the finder method is arbitrary and carries no semantics.But:

  • the return type determines the entity class to be queried, and

  • the parameters of the method must match the fields of the entity classexactly, by both name and type.

Considering our first example,Book has a persistent fieldString isbn, so this finder method is legal.If there were no field namedisbn inBook, or if it had a different type, this method declaration would be rejected with a meaningful error at compile time.Similarly, the second example is legal, sinceBook has fieldsString title andType type.

You might notice that our solution to this problem is very different from the approach taken by others.In DAO-style repository frameworks, you’re asked to encode the semantics of the finder method into thename of the method.This idea came to Java from Ruby, and we think it doesn’t belong here.It’s completely unnatural in Java, and by almost any measure other thancounting characters it’s objectively worse than just writing the query in a string literal.At least string literals accommodate whitespace and punctuation characters.Oh and, you know, it’s pretty useful to be able to rename a finder methodwithout changing its semantics. 🙄

The code generated for this finder method depends on what kind of fields match the method parameters:

@Id field

UsesEntityManager.find()

All@NaturalId fields

UsesSession.byNaturalId()

Other persistent fields, or a mix of field types

Uses a criteria query

The generated code also depends on what kind of session we have, since the capabilities of stateless sessions, and of reactive sessions, differ slightly from the capabilities of regular stateful sessions.

WithEntityManager as the session type, we obtain:

/** * Find {@link Book} by {@link Book#isbn isbn}. * * @see org.example.Dao#getBook(String) **/@OverridepublicBookgetBook(@NonnullStringisbn){returnentityManager.find(Book.class,isbn);}/** * Find {@link Book} by {@link Book#title title} and {@link Book#type type}. * * @see org.example.Dao#getBooksByTitle(String,Type) **/@OverridepublicList<Book>getBooksByTitle(Stringtitle,Typetype){varbuilder=entityManager.getEntityManagerFactory().getCriteriaBuilder();varquery=builder.createQuery(Book.class);varentity=query.from(Book.class);query.where(title==null?entity.get(Book_.title).isNull():builder.equal(entity.get(Book_.title),title),type==null?entity.get(Book_.type).isNull():builder.equal(entity.get(Book_.type),type));returnentityManager.createQuery(query).getResultList();}

It’s even possible to match a parameter of a finder method against a property of an associated entity or embeddable.The natural syntax would be a parameter declaration likeString publisher.name, but because that’s not legal Java, we can write it asString publisher$name, taking advantage of a legal Java identifier character that nobody ever uses for anything else:

@FindList<Book>getBooksByPublisherName(Stringpublisher$name);

The@Pattern annotation may be applied to a parameter of typeString, indicating that the argument is a wildcarded pattern which will be compared usinglike.

@FindList<Book>getBooksByTitle(@PatternStringtitle,Typetype);

Even better, a parameter may be of typeRange<T>, whereT is the type of the matching field.

@FindList<Book>getBooksByTitle(Range<String>title,Typetype);

TheRange interface has a variety ofstatic methods the caller may use to construct different kinds of ranges.For example,Range.pattern() constructs aRange representing a pattern.

List<Book>books=// returns books with titles beginning with "hibernate"queries.getBooksByTitle(Range.prefix("hibernate",false),type);

A finder method may specifyfetch profiles, for example:

@Find(namedFetchProfiles=Book_.FETCH_WITH_AUTHORS)BookgetBookWithAuthors(Stringisbn);

This lets us declare which associations ofBook should be pre-fetched by annotating theBook class.

7.7. Paging, ordering, and restrictions

Optionally, a query method—​or a finder method which returns multiple results—​may have additional "magic" parameters which do not map to query parameters:

Parameter typePurposeExample argument

Page

Specifies a page of query results

Page.first(20)

Order<? super E>

Specifies an entity attribute to order by, ifE is the entity type returned by the query

Order.asc(Book_.title)

List<Order? super E>
(or varargs)

Specifies entity attributes to order by, ifE is the entity type returned by the query

List.of(Order.asc(Book_.title), Order.asc(Book_.isbn))

Order<Object[]>

Specifies a column to order by, if the query returns a projection list

Order.asc(1)

List<Object[]>
(or varargs)

Specifies columns to order by, if the query returns a projection list

List.of(Order.asc(1), Order.desc(2))

Restriction<? super E>

Specifies a restriction used to filter query results

Restriction.startsWith("Hibernate")

Thus, if we redefine our earlier query method as follows:

interfaceQueries{@HQL("from Book where title like :title and type = :type")List<Book>findBooksByTitleAndType(Stringtitle,Typetype,Pagepage,Order<?superBook>...order);}

Then we can call it like this:

List<Book>books=Queries_.findBooksByTitleAndType(entityManager,titlePattern,Type.BOOK,Page.page(RESULTS_PER_PAGE,page),Order.asc(Book_.isbn));

Alternatively, we could have written this query method as a finder method:

interfaceQueries{@FindList<Book>getBooksByTitle(Stringtitle,Typetype,Pagepage,Order<?superBook>...order);}

Similarly, we may define a query method which accepts an arbitraryRestriction:

interfaceQueries{@FindList<Book>findBooks(Restriction<?superBook>restriction,Order<?superBook>...order);}

As wesaw earlier, theRestriction interface has a variety ofstatic methods for constructing restrictions.

List<Book>books=// returns books with titles beginning with "hibernate", sorted by titlequeries.findBooks(Restriction.startsWith(Book_.title,"hibernate",false),Order.asc(Book_.title));

This gives some dynamic control over query execution.We’ll seebelow that it’s even possible for the caller to gain direct control over theQuery object.

7.8. Key-based pagination

A generated query or finder method can make use ofkey-based pagination.

@Query("where publicationDate > :minDate")KeyedResultList<Book>booksFromDate(Sessionsession,LocalDateminDate,KeyedPage<Book>page);

Note that this method:

  • accepts aKeyedPage, and

  • returnsKeyedResultList.

Such a method may be used like this:

// obtain the first page of resultsKeyedResultList<Book>first=Queries_.booksFromDate(session,minDate,Page.first(25).keyedBy(Order.asc(Book_.isbn)));List<Book>firstPage=first.getResultList();...if(!firstPage.isLastPage()){// obtain the second page of resultsKeyedResultList<Book>second=Queries_.booksFromDate(session,minDate,firstPage.getNextPage());List<Book>secondPage=second.getResultList();...}

7.9. Query and finder method return types

A query method doesn’t need to returnList.It might return a singleBook.

@HQL("where isbn = :isbn")BookfindBookByIsbn(Stringisbn);

For a query with a projection list,Object[] orList<Object[]> is permitted:

@HQL("select isbn, title from Book where isbn = :isbn")Object[]findBookAttributesByIsbn(Stringisbn);

But when there’s just one item in theselect list, the type of that item should be used:

@HQL("select title from Book where isbn = :isbn")StringgetBookTitleByIsbn(Stringisbn);
@HQL("select local datetime")LocalDateTimegetServerDateTime();

A query which returns a selection list may have a query method which repackages the result as a record, as we saw inRepresenting projection lists.

recordIsbnTitle(Stringisbn,Stringtitle){}@HQL("select isbn, title from Book")List<IsbnTitle>listIsbnAndTitleForEachBook(Pagepage);

A query method might even returnTypedQuery orSelectionQuery:

@HQL("where title like :title")SelectionQuery<Book>findBooksByTitle(Stringtitle);

This is extremely useful at times, since it allows the client to further manipulate the query:

List<Book>books=Queries_.findBooksByTitle(entityManager,titlePattern).setOrder(Order.asc(Book_.title))// order the results.setPage(Page.page(RESULTS_PER_PAGE,page))// return the given page of results.setFlushMode(FlushModeType.COMMIT)// don't flush session before query execution.setReadOnly(true)// load the entities in read-only mode.setCacheStoreMode(CacheStoreMode.BYPASS)// don't cache the results.setComment("Hello world!")// add a comment to the generated SQL.getResultList();

Aninsert,update, ordelete query must returnint,boolean, orvoid.

@HQL("delete from Book")intdeleteAllBooks();
@HQL("update Book set discontinued = true where discontinued = false and isbn = :isbn")booleandiscontinueBook(Stringisbn);
@HQL("update Book set discontinued = true where isbn = :isbn")voiddiscontinueBook(Stringisbn);

On the other hand, finder methods are currently much more limited.A finder method must return an entity type likeBook, or a list of the entity type,List<Book>, for example.

As you might expect, for a reactive session, all query methods and finder methods must returnUni.

7.10. An alternative approach

What if you just don’t like the ideas we’ve presented in this chapter, preferring to call theSession orEntityManager directly, but you still want compile-time validation for HQL?Or what if youdo like the ideas, but you’re working on a huge existing codebase full of code you don’t want to change?

Well, there’s a solution for you, too.TheQuery Validator is a separate annotation processor that’s capable of type-checking HQL strings, not only in annotations, but even when they occur as arguments tocreateQuery(),createSelectionQuery(), orcreateMutationQuery(). It’s even able to check calls tosetParameter(), with some restrictions.

The Query Validator works injavac, Gradle, Maven, and the Eclipse Java Compiler.

Unlike Hibernate Processor, which is a completely bog-standard Java annotation processor based on only standard Java APIs, the Query Validator makes use of internal compiler APIs injavac andecj. This means it can’t be guaranteed to work in every Java compiler. The current release is known to work in JDK 11 and above, though JDK 15 or above is preferred.

8. Tuning and performance

Once you have a program up and running using Hibernate to accessthe database, it’s inevitable that you’ll find places where performance isdisappointing or unacceptable.

Fortunately, most performance problems are relatively easy to solve withthe tools that Hibernate makes available to you, as long as you keep acouple of simple principles in mind.

First and most important: the reason you’re using Hibernate isthat it makes things easier. If, for a certain problem, it’s makingthingsharder, stop using it. Solve this problem with a different toolinstead.

Just because you’re using Hibernate in your program doesn’t meanyou have to use iteverywhere.

Second: there are two main potential sources of performance bottlenecks ina program that uses Hibernate:

  • too many round trips to the database, and

  • memory consumption associated with the first-level (session) cache.

So performance tuning primarily involves reducing the number of accessesto the database, and/or controlling the size of the session cache.

But before we get to those more advanced topics, we should start by tuningthe connection pool.

8.1. Tuning the connection pool

The connection pool built in to Hibernate is suitable for testing, but isn’t intended for use in production.Instead, Hibernate supports several different connection pools, including our favorite, Agroal.

Hibernate will automatically make use ofAgroalConnectionProvider if the moduleorg.hibernate.orm:hibernate-agroal is available at runtime.So just add it as a runtime dependency, and you’re all set.

Well, actually, that’s a bit fragile, since Hibernate silently falls back to using the default connection pool if Agroal happens to be missing at runtime.Perhaps it’s better to set this configuration property:

Configuration property namePurpose

hibernate.connection.provider_class

Explicitly specify aconnection pool, for example,agroal,hikaricp orc3p0.

You can sethibernate.connection.provider_class toagroal so that Hibernate fails at startup if Agroal is missing.

To properly configure Agroal, you’ll need to set some extra configuration properties, in addition to the settings we already saw inBasic configuration settings.Properties with the prefixhibernate.agroal are passed through to Agroal:

# configure Agroal connection poolhibernate.agroal.maxSize20hibernate.agroal.minSize10hibernate.agroal.acquisitionTimeoutPT1shibernate.agroal.reapTimeoutPT10s

There are many to choose from, as enumerated byAgroalSettings:

Table 48. Settings for configuring Agroal
Configuration property namePurpose

hibernate.agroal.maxSize

The maximum number of connections present on the pool

hibernate.agroal.minSize

The minimum number of connections present on the pool

hibernate.agroal.initialSize

The number of connections added to the pool when it is started

hibernate.agroal.maxLifetime

The maximum amount of time a connection can live, after which it is removed from the pool

hibernate.agroal.acquisitionTimeout

The maximum amount of time a thread can wait for a connection, after which an exception is thrown instead

hibernate.agroal.reapTimeout

The duration for eviction of idle connections

hibernate.agroal.leakTimeout

The duration of time a connection can be held without causing a leak to be reported

hibernate.agroal.idleValidationTimeout

A foreground validation is executed if a connection has been idle on the pool for longer than this duration

hibernate.agroal.validationTimeout

The interval between background validation checks

hibernate.agroal.initialSql

A SQL command to be executed when a connection is created

The following settings are common to all connection pools supported by Hibernate:

Table 49. Common settings for connection pools

hibernate.connection.pool_size

The size of the connection pool

hibernate.connection.autocommit

The default autocommit mode

hibernate.connection.isolation

The default transaction isolation level

A popular alternative to Agroal is HikariCP.Its settings are enumerated byHikariCPSettings.

Container-managed datasources

In a container environment, you usually don’t need to configure a connection pool through Hibernate.Instead, you’ll use a container-managed datasource, as we saw inBasic configuration settings.

A related important setting is the default JDBC fetch size.

8.2. JDBC fetch size

TheJDBC fetch size controls the maximum number of rows the JDBC driver fetches from the database in one round trip.In Hibernate we usually limit query result sets usingpagination, and so we almost always prefer that the JDBC driver fetch the whole query result set in one trip.Most JDBC drivers accommodate this usage pattern bynot setting a default fetch size.However, there are a couple of exceptions to this and for the offending drivers you should probably override the default fetch size using the following configuration property.

Table 50. Default JDBC fetch size

hibernate.jdbc.fetch_size

The default JDBC fetch size

The default fetch size can be overridden for a given query by callingsetFetchSize(), but this is rarely necessary.

The Oracle JDBC driver defaults to a JDBC fetch size of 10.You shouldalways set explicitlyhibernate.jdbc.fetch_size if you’re using Oracle, or, even better, specify the parameterdefaultRowPrefetch in the JDBC connection URL.

8.3. Enabling statement batching

An easy way to improve performance of some transactions, with almost no work at all, is to turn on automatic DML statement batching.Batching only helps in cases where a program executes many inserts, updates, or deletes against the same table in a single transaction.

All we need to do is set a single property:

Table 51. Enabling JDBC batching
Configuration property namePurposeAlternative

hibernate.jdbc.batch_size

Maximum batch size for SQL statement batching

setJdbcBatchSize()

That said, batching is rarely the most convenient or most efficient way to update or delete many rows at once.

Even better than DML statement batching is the use of HQLupdate ordelete queries, or even native SQL that calls a stored procedure!

8.4. Association fetching

Achieving high performance in ORM means minimizing the number of round trips to the database. This goal should be uppermost in your mind whenever you’re writing data access code with Hibernate. The most fundamental rule of thumb in ORM is:

  • explicitly specify all the data you’re going to need right at the start of a session/transaction, and fetch it immediately in one or two queries,

  • and only then start navigating associations between persistent entities.

Fetching process

Without question, the most common cause of poorly-performing data access code in Java programs is the problem ofN+1 selects.Here, a list ofN rows is retrieved from the database in an initial query, and then associated instances of a related entity are fetched usingN subsequent queries.

This isn’t a bug or limitation of Hibernate; this problem even affects typical handwritten JDBC code behind DAOs.Only you, the developer, can solve this problem, because only you know ahead of time what data you’re going to need in a given unit of work.But that’s OK.Hibernate gives you all the tools you need.

In this section we’re going to discuss different ways to avoid such "chatty" interaction with the database.

Hibernate provides several strategies for efficiently fetching associations and avoidingN+1 selects:

  • outer join fetching—where an association is fetched using aleft outer join,

  • batch fetching—where an association is fetched using a subsequentselect with a batch of primary keys, and

  • subselect fetching—where an association is fetched using a subsequentselect with keys re-queried in a subselect.

Of these, you should almost always use outer join fetching.But let’s consider the alternatives first.

8.5. Batch fetching and subselect fetching

Consider the following code:

List<Book>books=session.createSelectionQuery("from Book order by isbn",Book.class).getResultList();books.forEach(book->book.getAuthors().forEach(author->out.println(book.title+" by "+author.name)));

This code isvery inefficient, resulting, by default, in the execution ofN+1select statements, whereN is the number ofBooks.

Let’s see how we can improve on that.

SQL for batch fetching

With batch fetching enabled, Hibernate might execute the following SQL on PostgreSQL:

/* initial query for Books */selectb1_0.isbn,b1_0.price,b1_0.published,b1_0.publisher_id,b1_0.titlefromBookb1_0orderbyb1_0.isbn/* first batch of associated Authors */selecta1_0.books_isbn,a1_1.id,a1_1.bio,a1_1.namefromBook_Authora1_0joinAuthora1_1ona1_1.id=a1_0.authors_idwherea1_0.books_isbn=any(?)/* second batch of associated Authors */selecta1_0.books_isbn,a1_1.id,a1_1.bio,a1_1.namefromBook_Authora1_0joinAuthora1_1ona1_1.id=a1_0.authors_idwherea1_0.books_isbn=any(?)

The firstselect statement queries and retrievesBooks.The second and third queries fetch the associatedAuthors in batches.The number of batches required depends on the configuredbatch size.Here, two batches were required, so two SQL statements were executed.

The SQL for batch fetching looks slightly different depending on the database.Here, on PostgreSQL, Hibernate passes a batch of primary key values as a SQLARRAY.

SQL for subselect fetching

On the other hand, with subselect fetching, Hibernate would execute this SQL:

/* initial query for Books */selectb1_0.isbn,b1_0.price,b1_0.published,b1_0.publisher_id,b1_0.titlefromBookb1_0orderbyb1_0.isbn/* fetch all associated Authors */selecta1_0.books_isbn,a1_1.id,a1_1.bio,a1_1.namefromBook_Authora1_0joinAuthora1_1ona1_1.id=a1_0.authors_idwherea1_0.books_isbnin(selectb1_0.isbnfromBookb1_0)

Notice that the first query is re-executed in a subselect in the second query.The execution of the subselect is likely to be relatively inexpensive, since the data should already be cached by the database.Clever, huh?

Enabling the use of batch or subselect fetching

Both batch fetching and subselect fetching are disabled by default, but we may enable one or the other globally using properties.

Table 52. Configuration settings to enable batch and subselect fetching
Configuration property nameProperty valueAlternatives

hibernate.default_batch_fetch_size

A sensible batch size>1 to enable batch fetching

@BatchSize(),setFetchBatchSize()

hibernate.use_subselect_fetch

true to enable subselect fetching

@Fetch(SUBSELECT),setSubselectFetchingEnabled()

Alternatively, we can enable one or the other in a given session:

session.setFetchBatchSize(5);session.setSubselectFetchingEnabled(true);

We may request subselect fetching more selectively by annotating a collection or many-valued association with the@Fetch annotation.

@ManyToMany@Fetch(SUBSELECT)Set<Author>authors;

Note that@Fetch(SUBSELECT) has the same effect as@Fetch(SELECT), except after execution of a HQL or criteria query.But after query execution,@Fetch(SUBSELECT) is able to much more efficiently fetch associations.

Later, we’ll see how we can usefetch profiles to do this even more selectively.

That’s all there is to it.Too easy, right?

Sadly, that’s not the end of the story.While batch fetching mightmitigate problems involving N+1 selects, it won’t solve them.The truly correct solution is to fetch associations using joins.Batch fetching (or subselect fetching) can only be thebest solution in rare cases where outer join fetching would result in a cartesian product and a huge result set.

But batch fetching and subselect fetching have one important characteristic in common: they can be performedlazily.This is, in principle, pretty convenient.When we query data, and then navigate an object graph, lazy fetching saves us the effort of planning ahead.It turns out that this is a convenience we’re going to have to surrender.

8.6. Join fetching

Outer join fetching is usually the best way to fetch associations, and it’s what we use most of the time.Unfortunately, by its very nature, join fetching simply can’t be lazy.So to make use of join fetching, we must plan ahead.Our general advice is:

Avoid the use of lazy fetching, which is often the source of N+1 selects.

Now, we’re not saying that associations should be mapped for eager fetching by default!That would be a terrible idea, resulting in simple session operations that fetch almost the entire database.Therefore:

Most associations should be mapped for lazy fetching by default.

It sounds as if this tip is in contradiction to the previous one, but it’s not.It’s saying that you must explicitly specify eager fetching for associations precisely when and where they are needed.

If we need eager join fetching in some particular transaction, we have four different ways to specify that.

Passing a JPAEntityGraph

We’ve already seen this inEntity graphs and eager fetching

Specifying a namedfetch profile

We’ll discuss this approach later inNamed fetch profiles

Usingleft join fetch in HQL/JPQL

SeeA Guide to Hibernate Query Language for details

UsingFrom.fetch() in a criteria query

Same semantics asjoin fetch in HQL

Typically, a query is the most convenient option.Here’s how we can ask for join fetching in HQL:

List<Book>booksWithJoinFetchedAuthors=session.createSelectionQuery("from Book join fetch authors order by isbn").getResultList();

And this is the same query, written using the criteria API:

varbuilder=sessionFactory.getCriteriaBuilder();varquery=builder.createQuery(Book.class);varbook=query.from(Book.class);book.fetch(Book_.authors);query.select(book);query.orderBy(builder.asc(book.get(Book_.isbn)));List<Book>booksWithJoinFetchedAuthors=session.createSelectionQuery(query).getResultList();

Either way, a single SQLselect statement is executed:

selectb1_0.isbn,a1_0.books_isbn,a1_1.id,a1_1.bio,a1_1.name,b1_0.price,b1_0.published,b1_0.publisher_id,b1_0.titlefromBookb1_0join(Book_Authora1_0joinAuthora1_1ona1_1.id=a1_0.authors_id)onb1_0.isbn=a1_0.books_isbnorderbyb1_0.isbn

Much better!

Join fetching, despite its non-lazy nature, is clearly more efficient than either batch or subselect fetching, and this is the source of our recommendation to avoid the use of lazy fetching.

There’s one interesting case where join fetching becomes inefficient: when we fetch two many-valued associationsin parallel.Imagine we wanted to fetch bothAuthor.books andAuthor.royaltyStatements in some unit of work.Joining both collections in a single query would result in a cartesian product of tables, and a large SQL result set.Subselect fetching comes to the rescue here, allowing us to fetchbooks using a join, androyaltyStatements using a single subsequentselect.

Of course, an alternative way to avoid many round trips to the database is to cache the data we need in the Java client.If we’re expecting to find the associated data in a local cache, we probably don’t need join fetching at all.

But what if we can’t becertain that all associated data will be in the cache?In that case, we might be able to reduce the cost of cache misses by enabling batch fetching.

8.7. The second-level cache

A classic way to reduce the number of accesses to the database is to use a second-level cache, allowing data cached in memory to be shared between sessions.

By nature, a second-level cache tends to undermine the ACID properties of transaction processing in a relational database.Wedon’t use a distributed transaction with two-phase commit to ensure that changes to the cache and database happen atomically.So a second-level cache is often by far the easiest way to improve the performance of a system, but only at the cost of making it much more difficult to reason about concurrency.And so the cache is a potential source of bugs which are difficult to isolate and reproduce.

Therefore, by default, an entity is not eligible for storage in the second-level cache.We must explicitly mark each entity that will be stored in the second-level cache with the@Cache annotation fromorg.hibernate.annotations.

But that’s still not enough.Hibernate does not itself contain an implementation of a second-level cache, so it’s also necessary to configure an externalcache provider.

Caching is disabled by default.To minimize the risk of data loss, we force you to stop and think before any entity goes into the cache.

Hibernate segments the second-level cache into namedregions, one for each:

  • mapped entity hierarchy or

  • collection role.

For example, there might be separate cache regions forAuthor,Book,Author.books, andBook.authors.

Each region is permitted its own policies for expiry, persistence, and replication. These policies must be configured externally to Hibernate.

The appropriate policies depend on the kind of data an entity represents. For example, a program might have different caching policies for "reference" data, for transactional data, and for data used for analytics. Ordinarily, the implementation of those policies is the responsibility of the underlying cache implementation.

The second-level cache is never aware of any changes to data which are made externally to Hibernate.Updates made via direct JDBC—​or by some other program—​are never visible in the second-level cache.When such updates occur, we might need toexplicitly invalidate cached data.Alternatively, in cases where the program is able to tolerate somewhat stale data, an expiry policy might be an acceptable solution.

8.8. Specifying which data is cached

By default, no data is eligible for storage in the second-level cache.

An entity hierarchy or collection role may be assigned a region using the@Cache annotation.If no region name is explicitly specified, the region name is just the name of the entity class or collection role.

@Entity@Cache(usage=NONSTRICT_READ_WRITE,region="Publishers")classPublisher{...@Cache(usage=READ_WRITE,region="PublishedBooks")@OneToMany(mappedBy=Book_.PUBLISHER)Set<Book>books;...}

The cache defined by a@Cache annotation is automatically utilized by Hibernate to:

  • retrieve an entity by id whenfind() is called, or

  • to resolve an association by id.

The@Cache annotation must be specified on theroot class of an entity inheritance hierarchy.It’s an error to place it on a subclass entity.

The@Cache annotation always specifies aCacheConcurrencyStrategy, a policy governing access to the second-level cache by concurrent transactions.

Table 53. Cache concurrency
Concurrency policyInterpretationExplanation

READ_ONLY

  • Immutable data

  • Read-only access

Indicates that the cached object is immutable, and is never updated. If an entity with this cache concurrency is updated, an exception is thrown.

This is the simplest, safest, and best-performing cache concurrency strategy. It’s particularly suitable for so-called "reference" data.

NONSTRICT_READ_WRITE

  • Concurrent updates are extremely improbable

  • Read/write access with no locking

Indicates that the cached object is sometimes updated, but that it’s extremely unlikely that two transactions will attempt to update the same item of data at the same time.

This strategy does not use locks. When an item is updated, the cache is invalidated both before and after completion of the updating transaction. But without locking, it’s impossible to completely rule out the possibility of a second transaction storing or retrieving stale data in or from the cache during the completion process of the first transaction.

READ_WRITE

  • Concurrent updates are possible but not common

  • Read/write access using soft locks

Indicates a non-vanishing likelihood that two concurrent transactions attempt to update the same item of data simultaneously.

This strategy uses "soft" locks to prevent concurrent transactions from retrieving or storing a stale item from or in the cache during the transaction completion process. A soft lock is simply a marker entry placed in the cache while the updating transaction completes.

  • A second transaction may not read the item from the cache while the soft lock is present, and instead simply proceeds to read the item directly from the database, exactly as if a regular cache miss had occurred.

  • Similarly, the soft lock also prevents this second transaction from storing a stale item to the cache when it returns from its round trip to the database with something that might not quite be the latest version.

TRANSACTIONAL

  • Concurrent updates are frequent

  • Transactional access

Indicates that concurrent writes are common, and the only way to maintain synchronization between the second-level cache and the database is via the use of a fully transactional cache provider. In this case, the cache and the database must cooperate via JTA or the XA protocol, and Hibernate itself takes on little responsibility for maintaining the integrity of the cache.

Which policies make sense may also depend on the underlying second-level cache implementation.

JPA has a similar annotation, named@Cacheable.Unfortunately, it’s almost useless to us, since:

  • it provides no way to specify any information about the nature of the cached entity and how its cache should be managed, and

  • it may not be used to annotate associations, and so we can’t even use it to mark collection roles as eligible for storage in the second-level cache.

8.9. Caching by natural id

If our entity has anatural id, we can enable an additional cache, which holds cross-references from natural id to primary id, by annotating the entity@NaturalIdCache.By default, the natural id cache is stored in a dedicated region of the second-level cache, separate from the cached entity data.

@Entity@Cache(usage=READ_WRITE,region="Book")@NaturalIdCache(region="BookIsbn")classBook{...@NaturalIdStringisbn;@NaturalIdintprinting;...}

This cache is utilized when the entity is retrieved using one of the operations ofSession which performslookup by natural id.

Since the natural id cache doesn’t contain the actual state of the entity, it doesn’t make sense to annotate an entity@NaturalIdCache unless it’s already eligible for storage in the second-level cache, that is, unless it’s also annotated@Cache.

It’s worth noticing that, unlike the primary identifier of an entity, a natural id might be mutable.

We must now consider a subtlety that often arises when we have to deal with so-called "reference data", that is, data which fits easily in memory, and doesn’t change much.

8.10. Caching and association fetching

Let’s consider again ourPublisher class:

@Cache(usage=NONSTRICT_READ_WRITE,region="Publishers")@EntityclassPublisher{...}

Data about publishers doesn’t change very often, and there aren’t so many of them.Suppose we’ve set everything up so that the publishers are almostalways available in the second-level cache.

Then in this case we need to think carefully about associations of typePublisher.

@ManyToOnePublisherpublisher;

There’s no need for this association to be lazily fetched, since we’re expecting it to be available in memory, so we won’t set itfetch=LAZY.But on the other hand, if we leave it marked for eager fetching then, by default, Hibernate will often fetch it using a join.This places completely unnecessary load on the database.

The solution is the@Fetch annotation:

@ManyToOne@Fetch(SELECT)Publisherpublisher;

By annotating the association@Fetch(SELECT), we suppress join fetching, giving Hibernate a chance to find the associatedPublisher in the cache.

Therefore, we arrive at this rule of thumb:

Many-to-one associations to "reference data", or to any other data that will almost always be available in the cache, should be mappedEAGER,SELECT.

Other associations, as we’vealready made clear, should beLAZY.

Once we’ve marked an entity or collection as eligible for storage in the second-level cache, we still need to set up an actual cache.

8.11. Configuring the second-level cache provider

Configuring a second-level cache provider is a rather involved topic, and quite outside the scope of this document.But in case it helps, we often test Hibernate with the following configuration, which uses EHCache as the cache implementation, as above inOptional dependencies:

Table 54. EHCache configuration
Configuration property nameProperty value

hibernate.cache.region.factory_class

jcache

hibernate.javax.cache.uri

/ehcache.xml

If you’re using EHCache, you’ll also need to include anehcache.xml filethat explicitly configures the behavior of each cache region belonging toyour entities and collections.You’ll find more information about configuring EHCachehere.

We may use any other implementation of JCache, such asCaffeine.JCache automatically selects whichever implementation it finds on the classpath.If there are multiple implementations on the classpath, we must disambiguate using:

Table 55. Disambiguating the JCache implementation
Configuration property nameProperty value

hibernate.javax.cache.provider

The implementation ofjavax.cache.spi.CachingProvider, for example:

org.ehcache.jsr107.EhcacheCachingProvider

for EHCache

com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider

for Caffeine

Alternatively, to use Infinispan as the cache implementation, the following settings are required:

Table 56. Infinispan provider configuration
Configuration property nameProperty value

hibernate.cache.region.factory_class

infinispan

hibernate.cache.infinispan.cfg

Path to infinispan configuration file, for example:

org/infinispan/hibernate/cache/commons/builder/infinispan-configs.xml

for a distributed cache

org/infinispan/hibernate/cache/commons/builder/infinispan-configs-local.xml

to test with local cache

Infinispan is usually used when distributed caching is required.There’s more about using Infinispan with Hibernatehere.

Finally, there’s a way to globally disable the second-level cache:

Table 57. Setting to disable caching
Configuration property nameProperty value

hibernate.cache.use_second_level_cache

true to enable caching, orfalse to disable it

Whenhibernate.cache.region.factory_class is set, this property defaults totrue.

This setting lets us easily disable the second-level cache completely when troubleshooting or profiling performance.

You can find much more information about the second-level cache in theUser Guide.

8.12. Caching query result sets

The caches we’ve described above are only used to optimize lookups by id or by natural id.Hibernate also has a way to cache the result sets of queries, though this is only rarely an efficient thing to do.

The query cache must be enabled explicitly:

Table 58. Setting to enable the query cache
Configuration property nameProperty value

hibernate.cache.use_query_cache

true to enable the query cache

To cache the results of a query, callSelectionQuery.setCacheable(true):

session.createQuery("from Product where discontinued = false").setCacheable(true).getResultList();

By default, the query result set is stored in a cache region nameddefault-query-results-region.Since different queries should have different caching policies, it’s common to explicitly specify a region name:

session.createQuery("from Product where discontinued = false").setCacheable(true).setCacheRegion("ProductCatalog").getResultList();

A result set is cached together with alogical timestamp.By "logical", we mean that it doesn’t actually increase linearly with time, and in particular it’s not the system time.

When aProduct is updated, Hibernatedoes not go through the query cache and invalidate every cached result set that’s affected by the change.Instead, there’s a special region of the cache which holds a logical timestamp of the most-recent update to each table.This is called theupdate timestamps cache, and it’s kept in the regiondefault-update-timestamps-region.

It’syour responsibility to ensure that this cache region is configured with appropriate policies.In particular, update timestamps should never expire or be evicted.

When a query result set is read from the cache, Hibernate compares its timestamp with the timestamp of each of the tables that affect the results of the query, andonly returns the result set if the result set isn’t stale.If the result setis stale, Hibernate goes ahead and re-executes the query against the database and updates the cached result set.

As is generally the case with any second-level cache, the query cache can break the ACID properties of transactions.

8.13. Second-level cache management

For the most part, the second-level cache is transparent.Program logic which interacts with the Hibernate session is unaware of the cache, and is not impacted by changes to caching policies.

At worst, interaction with the cache may be controlled by specifying of an explicitCacheMode:

session.setCacheMode(CacheMode.IGNORE);

Or, using JPA-standard APIs:

entityManager.setCacheRetrieveMode(CacheRetrieveMode.BYPASS);entityManager.setCacheStoreMode(CacheStoreMode.BYPASS);

The JPA-defined cache modes come in two flavors:CacheRetrieveMode andCacheStoreMode.

Table 59. JPA-defined cache retrieval modes
ModeInterpretation

CacheRetrieveMode.USE

Read data from the cache if available

CacheRetrieveMode.BYPASS

Don’t read data from the cache; go direct to the database

We might selectCacheRetrieveMode.BYPASS if we’re concerned about the possibility of reading stale data from the cache.

Table 60. JPA-defined cache storage modes
ModeInterpretation

CacheStoreMode.USE

Write data to the cache when read from the database or when modified; do not update already-cached items when reading

CacheStoreMode.REFRESH

Write data to the cache when read from the database or when modified; always update cached items when reading

CacheStoreMode.BYPASS

Don’t write data to the cache

We should selectCacheStoreMode.BYPASS if we’re querying data that doesn’t need to be cached.

It’s a good idea to set theCacheStoreMode toBYPASS just before running a query which returns a large result set full of data that we don’t expect to need again soon.This saves work, and prevents the newly-read data from pushing out the previously cached data.

In JPA we would use this idiom:

entityManager.setCacheStoreMode(CacheStoreMode.BYPASS);List<Publisher>allpubs=entityManager.createQuery("from Publisher",Publisher.class).getResultList();entityManager.setCacheStoreMode(CacheStoreMode.USE);

But Hibernate has a better way:

List<Publisher>allpubs=session.createSelectionQuery("from Publisher",Publisher.class).setCacheStoreMode(CacheStoreMode.BYPASS).getResultList();

A HibernateCacheMode packages aCacheRetrieveMode with aCacheStoreMode.

Table 61. Hibernate cache modes and JPA equivalents
HibernateCacheModeEquivalent JPA modes

NORMAL

CacheRetrieveMode.USE,CacheStoreMode.USE

IGNORE

CacheRetrieveMode.BYPASS,CacheStoreMode.BYPASS

GET

CacheRetrieveMode.USE,CacheStoreMode.BYPASS

PUT

CacheRetrieveMode.BYPASS,CacheStoreMode.USE

REFRESH

CacheRetrieveMode.REFRESH,CacheStoreMode.BYPASS

There’s no particular reason to prefer Hibernate’sCacheMode over the JPA equivalents.This enumeration only exists because Hibernate had cache modes long before they were added to JPA.

For "reference" data, that is, for data which is expected to always be found in the second-level cache, it’s a good idea toprime the cache at startup.There’s a really easy way to do this: just execute a query immediately after obtaining theEntityManager orSessionFactory.

SessionFactorysessionFactory=setupHibernate(newConfiguration()).buildSessionFactory();// prime the second-level cachesessionFactory.inSession(session->{session.createSelectionQuery("from Country")).setReadOnly(true).getResultList();session.createSelectionQuery("from Product where discontinued = false")).setReadOnly(true).getResultList();});

Very occasionally, it’s necessary or advantageous to control the cache explicitly.For example, we might need to evict some data that we know to be stale because it was updated:

  • via direct JDBC, or

  • by some other program.

TheCache interface allows programmatic eviction of cached items.

sessionFactory.getCache().evictEntityData(Book.class,bookId);

Second-level cache management via theCache interface is not transaction-aware.None of the operations ofCache respect any isolation or transactional semantics associated with the underlying caches. In particular, eviction via the methods of this interface causes an immediate "hard" removal outside any current transaction and/or locking scheme.

Ordinarily, however, Hibernate automatically evicts or updates cached data after modifications, and, in addition, cached data which is unused will eventually be expired according to the configured policies.

This is quite different to what happens with the first-level cache.

8.14. Session cache management

Entity instances aren’t automatically evicted from the session cache when they’re no longer needed.Instead, they stay pinned in memory until the session they belong to is discarded by your program.

The methodsdetach() andclear() allow you to remove entities from the session cache, making them available for garbage collection.Since most sessions are rather short-lived, you won’t need these operations very often.And if you find yourself thinking youdo need them in a certain situation, you should strongly consider an alternative solution: astateless session.

8.15. Stateless sessions

An arguably-underappreciated feature of Hibernate is theStatelessSession interface, which provides a command-oriented, more bare-metal approach to interacting with the database.

You may obtain a stateless session from theSessionFactory:

StatelessSession ss = getSessionFactory().openStatelessSession();

A stateless session:

  • doesn’t have a first-level cache (persistence context), and

  • doesn’t implement transactional write-behind or automatic dirty checking, so all operations are executed immediately when they’re explicitly called.

For a stateless session, we’re always working with detached objects.Thus, the programming model is a bit different:

Table 62. Important methods of theStatelessSession
Method name and parametersEffect

get(Class, Object)

Obtain a detached object, given its type and its id, by executing aselect

fetch(Object)

Fetch an association of a detached object

refresh(Object)

Refresh the state of a detached object by executingaselect

insert(Object)

Immediatelyinsert the state of the given transient object into the database

update(Object)

Immediatelyupdate the state of the given detached object in the database

delete(Object)

Immediatelydelete the state of the given detached object from the database

upsert(Object)

Immediatelyinsert orupdate the state of the given detached object using a SQLmerge into statement

The operations of a stateless session have no correspondingCascadeTypes, and so these operations never cascade to associated entity instances.
There’s noflush() operation, and soupdate() is always explicit.

In certain circumstances, this makes stateless sessions easier to work with and simpler to reason about, but with the caveat that a stateless session is much more vulnerable to data aliasing effects, since it’s easy to get two non-identical Java objects which both represent the same row of a database table.

Consider the following fragments:

varb1=statelessSession.get(Book.class,isbn);varb2=statelessSession.get(Book.class,isbn);assertb1==b2;// fails
varb1=statelessSession.get(Book.class,isbn);varb2=statelessSession.get(Book.class,isbn);statelessSession.fetch(b1.publisher);statelessSession.fetch(b2.publisher);assertb1.publisher==b2.publisher;// fails

In a stateful session, entity instances are canonicalized by primary key, and so we don’t usually have two different objects representing a single row.No such canonicalization exists across invocations of a stateless session.In a stateless session, both the assertions fail.

If we usefetch() in a stateless session, we can very easily obtain two objects representing the same database row!

But there are also some advantages to this model.In particular, the absence of a persistence context means that we can safely perform bulk-processing tasks without allocating huge quantities of memory.Use of aStatelessSession alleviates the need to call:

  • clear() ordetach() to perform first-level cache management, and

  • setCacheMode() to bypass interaction with the second-level cache.

Stateless sessions can be useful, but for bulk operations on huge datasets, Hibernate can’t possibly compete with stored procedures!

8.16. Optimistic and pessimistic locking

Finally, an aspect of behavior under load that we didn’t mention above is row-level data contention.When many transactions try to read and update the same data, the program might become unresponsive with lock escalation, deadlocks, and lock acquisition timeout errors.

There’s two basic approaches to data concurrency in Hibernate:

  • optimistic locking using@Version columns, and

  • database-level pessimistic locking using the SQLfor update syntax (or equivalent).

In the Hibernate community it’smuch more common to use optimistic locking, and Hibernate makes that incredibly easy.

Where possible, in a multiuser system, avoid holding a pessimistic lock across a user interaction.Indeed, the usual practice is to avoid having transactions that span user interactions. For multiuser systems, optimistic locking is king.

That said, thereis also a place for pessimistic locks, which can sometimes reduce the probability of transaction rollbacks.

Therefore, thefind(),lock(), andrefresh() methods of the session accept an optionalLockMode.We can also specify aLockMode for a query.The lock mode can be used to request a pessimistic lock, or to customize the behavior of optimistic locking:

Table 63. Optimistic and pessimistic lock modes
LockMode typeMeaning

READ

An optimistic lock obtained implicitly wheneveran entity is read from the database usingselect

OPTIMISTIC

An optimistic lock obtained when an entity isread from the database, and verified using aselect to check the version when thetransaction completes

OPTIMISTIC_FORCE_INCREMENT

An optimistic lock obtained when an entity isread from the database, and enforced using anupdate to increment the version when thetransaction completes

WRITE

A pessimistic lock obtained implicitly wheneveran entity is written to the database usingupdate orinsert

PESSIMISTIC_READ

A pessimisticfor share lock

PESSIMISTIC_WRITE

A pessimisticfor update lock

PESSIMISTIC_FORCE_INCREMENT

A pessimistic lock enforced using an immediateupdate to increment the version

NONE

No lock; assigned when an entity is read from the second-level cache

Note that anOPTIMISTIC lock is always verified at the end of the transaction, even when the entity has not been modified.This is slightly different to what most people mean when they talk about an "optimistic lock".It’s never necessary to request anOPTIMISTIC lock on a modified entity, since the version number is always verified when a SQLupdate is executed.

JPA has its ownLockModeType, which enumerates most of the same modes.However, JPA’sLockModeType.READ is a synonym forOPTIMISTIC — it’s not the same as Hibernate’sLockMode.READ.Similarly,LockModeType.WRITE is a synonym forOPTIMISTIC_FORCE_INCREMENT and is not the same asLockMode.WRITE.

8.17. Collecting statistics

We may ask Hibernate to collect statistics about its activity by setting this configuration property:

Configuration property nameProperty value

hibernate.generate_statistics

true to enable collection of statistics

The statistics are exposed by theStatistics object:

longfailedVersionChecks=sessionFactory.getStatistics().getOptimisticFailureCount();longpublisherCacheMissCount=sessionFactory.getStatistics().getEntityStatistics(Publisher.class.getName()).getCacheMissCount()

Hibernate’s statistics enable observability.BothMicrometer andSmallRye Metrics are capable of exposing these metrics.

8.18. Using Java Flight Recorder

Hibernate JFR is a separate module which reports events toJava Flight Recorder.This is different to reporting aggregatedmetrics via a tool like Micrometer, since JFR records information about the timing and duration of each discrete event, along with a stack trace.If anything, the information reported by JFR is a littletoo detailed to make it really useful for performance tuning—​it’s perhaps more useful fortroubleshooting.

No special configuration is required to use Hibernate JFR.Just includeorg.hibernate.orm:hibernate-jfr as a runtime dependency.In particular, youdon’t need to enablehibernate.generate_statistics.

8.19. Tracking down slow queries

When a poorly-performing SQL query is discovered in production, it can sometimes be hard to track down exactly where in the Java code the query originates.Hibernate offers two configuration properties that can make it easier to identify a slow query and find its source.

Table 64. Settings for tracking slow queries
Configuration property namePurposeProperty value

hibernate.log_slow_query

Log slow queries at theINFO level

The minimum execution time, in milliseconds, which characterizes a "slow" query

hibernate.use_sql_comments

Prepend comments to the executed SQL

true orfalse

Whenhibernate.use_sql_comments is enabled, the text of the HQL query is prepended as a comment to the generated SQL, which usually makes it easy to find the HQL in the Java code.

The comment text may be customized:

  • by callingQuery.setComment(comment) orQuery.setHint(AvailableHints.HINT_COMMENT,comment), or

  • via the@NamedQuery annotation.

Once you’ve identified a slow query, one of the best ways to make it faster is toactually go and talk to someone who is an expert at making queries go fast.These people are called "database administrators", and if you’re reading this document you probably aren’t one.Database administrators know lots of stuff that Java developers don’t.So if you’re lucky enough to have a DBA about, you don’t need to Dunning-Kruger your way out of a slow query.

An expertly-defined index might be all you need to fix a slow query.

8.20. Adding indexes

The@Index annotation may be used to add an index to a table:

@Entity@Table(indexes=@Index(columnList="title, year, publisher_id"))classBook{...}

It’s even possible to specify an ordering for an indexed column, or that the index should be case-insensitive:

@Entity@Table(indexes=@Index(columnList="(lower(title)), year desc, publisher_id"))classBook{...}

This lets us create a customized index for a particular query.

Note that SQL expressions likelower(title) must be enclosed in parentheses in thecolumnList of the index definition.

It’s not clear that information about indexes belongs in annotations of Java code.Indexes are usually maintained and modified by a database administrator, ideally by an expert in tuning the performance of one particular RDBMS.So it might be better to keep the definition of indexes in a SQL DDL script that your DBA can easily read and modify.Remember, we can ask Hibernate to execute a DDL script using the propertyjavax.persistence.schema-generation.create-script-source.

8.21. Dealing with denormalized data

A typical relational database table in a well-normalized schema has a relatively small number of columns, and so there’s little to be gained by selectively querying columns and populating only certain fields of an entity class.

But occasionally, we hear from someone asking how to map a table with a hundred columns or more!This situation can arise when:

  • data is intentionally denormalized for performance,

  • the results of a complicated analytic query are exposed via a view, or

  • someone has done something crazy and wrong.

Let’s suppose that we’renot dealing with the last possibility.Then we would like to be able to query the monster table without returning all of its columns.At first glance, Hibernate doesn’t offer a perfect bottled solution to this problem.This first impression is misleading.Actually, Hibernate features more than one way to deal with this situation, and the real problem is deciding between the ways.We could:

  1. map multiple entity classes to the same table or view, being careful about "overlaps" where a mutable column is mapped to more than one of the entities,

  2. useHQL ornative SQL queries returningresults into record types instead of retrieving entity instances, or

  3. use thebytecode enhancer and@LazyGroup for attribute-level lazy fetching.

Some other ORM solutions push the third option as the recommended way to handle huge tables, but this has never been the preference of the Hibernate team or Hibernate community.It’s much more typesafe to use one of the first two options.

8.22. Reactive programming with Hibernate

Finally, many systems which require high scalability now make use of reactive programming and reactive streams.Hibernate Reactive brings O/R mapping to the world of reactive programming.You can learn much more about Hibernate Reactive from itsReference Documentation.

Hibernate Reactive may be used alongside vanilla Hibernate in the same program, and can reuse the same entity classes.This means you can use the reactive programming model exactly where you need it—perhaps only in one or two places in your system.You don’t need to rewrite your whole program using reactive streams.

9. Advanced Topics

In the last chapter of this Introduction, we turn to some topics that don’t really belong in an introduction.Here we consider some problems, and solutions, that you’re probably not going to run into immediately if you’re new to Hibernate.But we do want you to knowabout them, so that when the time comes, you’ll know what tool to reach for.

9.1. Filters

Filters are one of the nicest and under-usedest features of Hibernate, and we’re quite proud of them.A filter is a named, globally-defined, parameterized restriction on the data that is visible in a given session.

Examples of well-defined filters might include:

  • a filter that restricts the data visible to a given user according to row-level permissions,

  • a filter which hides data which has been soft-deleted,

  • in a versioned database, a filter that displays versions which were current at a given instant in the past, or

  • a filter that restricts to data associated with a certain geographical region.

A filter must be declared somewhere.A package descriptor is as good a place as any for a@FilterDef:

@FilterDef(name="ByRegion",parameters=@ParamDef(name="region",type=String.class))packageorg.hibernate.example;

This filter has one parameter.Fancier filters might in principle have multiple parameters, though we admit this must be quite rare.

If you add annotations to a package descriptor, and you’re usingConfiguration to configure Hibernate, make sure you callConfiguration.addPackage() to let Hibernate know that the package descriptor is annotated.

Typically, but not necessarily, a@FilterDef specifies a default restriction:

@FilterDef(name="ByRegion",parameters=@ParamDef(name="region",type=String.class),defaultCondition="region = :region")packageorg.hibernate.example;

Note that filter restrictions are always written in the native SQL dialect of the database,not in HQL.

The restriction must contain a reference to the parameter of the filter, specified using the usual syntax for named parameters.

Any entity or collection which is affected by a filter must be annotated@Filter:

@Entity@Filter(name=example_.BY_REGION)classUser{@IdStringusername;Stringregion;...}

Here, as usual,example_.BY_REGION is generated by Hibernate Processor, and is just a constant with the value"ByRegion".

If the@Filter annotation does not explicitly specify a restriction, the default restriction given by the@FilterDef will be applied to the entity.But an entity is free to override the default condition.

@Entity@Filter(name=example_.FILTER_BY_REGION,condition="name = :region")classRegion{@IdStringname;...}

Note that the restriction specified by thecondition ordefaultCondition is a native SQL expression.

Table 65. Annotations for defining filters
AnnotationPurpose

@FilterDef

Defines a filter and declares its name (exactly one per filter)

@Filter

Specifies how a filter applies to a given entity or collection (many per filter)

A filtercondition may not specify joins to other tables, but it may contain a subquery.

@Filter(name="notDeleted"condition="(select r.deletionTimestamp from Record r where r.id = record_id) is not null")

Only unqualified column names likerecord_id in this example are interpreted as belonging to the table of the filtered entity.

By default, a new session comes with every filter disabled.A filter may be explicitly enabled in a given session by callingenableFilter() and assigning arguments to the parameters of the filter using the returned instance ofFilter.You should do this right at thestart of the session.

sessionFactory.inTransaction(session->{session.enableFilter(example_.FILTER_BY_REGION).setParameter("region","es").validate();...});

Now, any queries executed within the session will have the filter restriction applied.Collections annotated@Filter will also have their members correctly filtered.

On the other hand, filters are not applied to@ManyToOne associations, nor tofind().This is completely by design and is not in any way a bug.

More than one filter may be enabled in a given session.

Alternatively, since Hibernate 6.5, a filter may be declared asautoEnabled in every session.In this case, the argument to a filter parameter must be obtained from aSupplier.

@FilterDef(name="ByRegion",autoEnabled=true,parameters=@ParamDef(name="region",type=String.class,resolver=RegionSupplier.class),defaultCondition="region = :region")packageorg.hibernate.example;

It’s not necessary to callenableFilter() for a filter declaredautoEnabled = true.

When we only need to filter rows by a static condition with no parameters, we don’t need a filter, since@SQLRestriction provides a much simpler way to do that.

We’ve mentioned that a filter can be used to implement versioning, and to providehistorical views of the data.Being such a general-purpose construct, filters provide a lot of flexibility here.But if you’re after a more focused/opinionated solution to this problem, you should definitely check outEnvers.

Using Envers for auditing historical data

Envers is an add-on to Hibernate ORM which keeps a historical record of each versioned entity in a separateaudit table, and allows past revisions of the data to be viewed and queried.A full introduction to Envers would require a whole chapter, so we’ll just give you a quick taste here.

First, we must mark an entity as versioned, using the@Audited annotation:

@Audited@Entity@Table(name="CurrentDocument")@AuditTable("DocumentRevision")classDocument{...}

The@AuditTable annotation is optional, and it’s better to set eitherorg.hibernate.envers.audit_table_prefix ororg.hibernate.envers.audit_table_suffix and let the audit table name be inferred.

TheAuditReader interface exposes operations for retrieving and querying historical revisions.It’s really easy to get hold of one of these:

AuditReaderreader=AuditReaderFactory.get(entityManager);

Envers tracks revisions of the data via a globalrevision number.We may easily find the revision number which was current at a given instant:

Numberrevision=reader.getRevisionNumberForDate(datetime);

We can use the revision number to ask for the version of our entity associated with the given revision number:

Documentdoc=reader.find(Document.class,id,revision);

Alternatively, we can directly ask for the version which was current at a given instant:

Documentdoc=reader.find(Document.class,id,datetime);

We can even execute queries to obtain lists of entities current at the given revision number:

Listdocuments=reader.createQuery().forEntitiesAtRevision(Document.class,revision).getResultList();

For much more information, see theUser Guide.

Historically, filters where often used to implement soft-delete.But, since 6.4, Hibernate now comes with soft-delete built in.

9.2. Soft-delete

Even when we don’t need complete historical versioning, we often prefer to "delete" a row by marking it as obsolete using a SQLupdate, rather than by executing an actual SQLdelete and removing the row from the database completely.

The@SoftDelete annotation controls how this works:

@Entity@SoftDelete(columnName="deleted",converter=TrueFalseConverter.class)classDraft{...}

ThecolumnName specifies a column holding the deletion status, and theconverter is responsible for converting a JavaBoolean to the type of that column.In this example,TrueFalseConverter sets the column to the character'F' initially, and to'T' when the row is deleted.Any JPAAttributeConverter for the JavaBoolean type may be used here.Built-in options includeNumericBooleanConverter andYesNoConverter.

Much more information about soft delete is available in theUser Guide.

Another feature that youcould use filters for, but now don’t need to, is multi-tenancy.

9.3. Multi-tenancy

Amulti-tenant database is one where the data is segregated bytenant.We don’t need to actually define what a "tenant" really represents here; all we care about at this level of abstraction is that each tenant may be distinguished by a unique identifier.And that there’s a well-definedcurrent tenant in each session.

We may specify the current tenant when we open a session:

varsession=sessionFactory.withOptions().tenantIdentifier(tenantId).openSession();

Or, when using JPA-standard APIs:

varentityManager=entityManagerFactory.createEntityManager(Map.of(HibernateHints.HINT_TENANT_ID,tenantId));

However, since we often don’t have this level of control over creation of the session, it’s more common to supply an implementation ofCurrentTenantIdentifierResolver to Hibernate.

There are three common ways to implement multi-tenancy:

  1. each tenant has its own database,

  2. each tenant has its own schema, or

  3. tenants share tables in a single schema, and rows are tagged with the tenant id.

From the point of view of Hibernate, there’s little difference between the first two options.Hibernate will need to obtain a JDBC connection with permissions on the database and schema owned by the current tenant.

Therefore, we must implement aMultiTenantConnectionProvider which takes on this responsibility:

  • from time to time, Hibernate will ask for a connection, passing the id of the current tenant, and then we must create an appropriate connection or obtain one from a pool, and return it to Hibernate, and

  • later, Hibernate will release the connection and ask us to destroy it or return it to the appropriate pool.

The third option is quite different.In this case we don’t need aMultiTenantConnectionProvider, but we will need a dedicated column holding the tenant id mapped by each of our entities.

@EntityclassAccount{@IdStringid;@TenantIdStringtenantId;...}

The@TenantId annotation is used to indicate an attribute of an entity which holds the tenant id.Within a given session, our data is automatically filtered so that only rows tagged with the tenant id of the current tenant are visible in that session.

Native SQL queries arenot automatically filtered by tenant id; you’ll have to do that part yourself.

To make use of multi-tenancy, we’ll usually need to set at least one of these configuration properties:

Table 66. Multi-tenancy configuration
Configuration property namePurpose

hibernate.tenant_identifier_resolver

Specifies theCurrentTenantIdentifierResolver

hibernate.multi_tenant_connection_provider

Specifies theMultiTenantConnectionProvider

Do not configure those properties if you would like the configuredBeanContainer provide the implementation.A longer discussion of multi-tenancy may be found in theUser Guide.

9.4. Using custom-written SQL

We’ve already discussed how to runqueries written in SQL, but occasionally that’s not enough.Sometimes—but much less often than you might expect—we would like to customize the SQL used by Hibernate to perform basic CRUD operations for an entity or collection.

For this we can use@SQLInsert and friends:

@Entity@SQLInsert(sql="insert into person (name, id, valid) values (?, ?, true)",verify=Expectation.RowCount.class)@SQLUpdate(sql="update person set name = ? where id = ?")@SQLDelete(sql="update person set valid = false where id = ?")@SQLSelect(sql="select id, name from person where id = ? and valid = true")publicstaticclassPerson{...}
Table 67. Annotations for overriding generated SQL
AnnotationPurpose

@SQLSelect

Overrides a generated SQLselect statement

@SQLInsert

Overrides a generated SQLinsert statement

@SQLUpdate

Overrides a generated SQLupdate statement

@SQDelete

Overrides a generated SQLdelete statement for a single row

@SQDeleteAll

Overrides a generated SQLdelete statement for multiple rows

@SQLRestriction

Adds a restriction to generated SQL

@SQLOrder

Adds an ordering to generated SQL

If the custom SQL should be executed via aCallableStatement, just specifycallable=true.

Any SQL statement specified by one of these annotations must have exactly the number of JDBC parameters that Hibernate expects, that is, one for each column mapped by the entity, in the exact order Hibernate expects. In particular, the primary key columns must come last.

However, the@Column annotation does lend some flexibility here:

  • if a column should not be written as part of the custominsert statement, and has no corresponding JDBC parameter in the custom SQL, map it@Column(insertable=false), or

  • if a column should not be written as part of the customupdate statement, and has no corresponding JDBC parameter in the custom SQL, map it@Column(updatable=false).

Theverify member of these annotations specifies a class implementingExpectation, allowing customized logic for checking the success of an operation executed via JDBC.There are three built-in implementations:

  • Expectation.None, which performs no checks,

  • Expectation.RowCount, which is what Hibernate usually uses when executing its own generated SQL,

  • andExpectation.OutParameter, which is useful for checking an output parameter of a stored procedure.

You can write your own implementation ofExpectation if none of these options is suitable.

If you need custom SQL, but are targeting multiple dialects of SQL, you can use the annotations defined inDialectOverride.For example, this annotation lets us override the custominsert statement just for PostgreSQL:

@DialectOverride.SQLInsert(dialect=PostgreSQLDialect.class,override=@SQLInsert(sql="insert into person (name,id) values (?,gen_random_uuid())"))

It’s even possible to override the custom SQL for specificversions of a database.

Sometimes a custominsert orupdate statement assigns a value to a mapped column which is calculated when the statement is executed on the database.For example, the value might be obtained by calling a SQL function:

@SQLInsert(sql="insert into person (name, id) values (?, gen_random_uuid())")

But the entity instance which represents the row being inserted or updated won’t be automatically populated with that value.And so our persistence context loses synchronization with the database.In situations like this, we may use the@Generated annotation to tell Hibernate to reread the state of the entity after eachinsert orupdate.

9.5. Handling database-generated columns

Sometimes, a column value is assigned or mutated by events that happen in the database, and aren’t visible to Hibernate.For example:

  • a table might have a column value populated by a trigger,

  • a mapped column might have a default value defined in DDL, or

  • a custom SQLinsert orupdate statement might assign a value to a mapped column, as we saw in the previous subsection.

One way to deal with this situation is to explicitly callrefresh() at appropriate moments, forcing the session to reread the state of the entity.But this is annoying.

The@Generated annotation relieves us of the burden of explicitly callingrefresh().It specifies that the value of the annotated entity attribute is generated by the database, and that the generated value should be automatically retrieved using a SQLreturning clause, or separateselect after it is generated.

A useful example is the following mapping:

@EntityclassEntity{@Generated@Id@ColumnDefault("gen_random_uuid()")UUIDid;}

The generated DDL is:

createtableEntity(iduuiddefaultgen_random_uuid()notnull,primarykey(uuid))

So here the value ofid is defined by the column default clause, by calling the PostgreSQL functiongen_random_uuid().

When a column value is generated during updates, use@Generated(event=UPDATE).When a value is generated by both insertsand updates, use@Generated(event={INSERT,UPDATE}).

For columns which should be generated using a SQLgenerated always as clause, prefer the@GeneratedColumn annotation, so that Hibernate automatically generates the correct DDL.

Actually, the@Generated and@GeneratedColumn annotations are defined in terms of a more generic and user-extensible framework for handling attribute values generated in Java, or by the database.So let’s drop down a layer, and see how that works.

9.6. User-defined generators

JPA doesn’t define a standard way to extend the set of id generation strategies, but Hibernate does:

  • theGenerator hierarchy of interfaces in the packageorg.hibernate.generator lets you define new generators, and

  • the@IdGeneratorType meta-annotation from the packageorg.hibernate.annotations lets you write an annotation which associates aGenerator type with identifier attributes.

Furthermore, the@ValueGenerationType meta-annotation lets you write an annotation which associates aGenerator type with a non-@Id attribute.

These APIs were new in Hibernate 6, and supersede the classicIdentifierGenerator interface and@GenericGenerator annotation from older versions of Hibernate.However, the older APIs are still available and customIdentifierGenerators written for older versions of Hibernate continue to work in Hibernate 7.

Hibernate has a range of built-in generators which are defined in terms of this new framework.

Table 68. Built-in generators
AnnotationImplementationPurpose

@Generated

GeneratedGeneration

Generically handles database-generated values

@GeneratedColumn

GeneratedAlwaysGeneration

Handles values generated usinggenerated always

@CurrentTimestamp

CurrentTimestampGeneration

Generic support for database or in-memory generation of creation or update timestamps

@CreationTimestamp

CurrentTimestampGeneration

A timestamp generated when an entity is first made persistent

@UpdateTimestamp

CurrentTimestampGeneration

A timestamp generated when an entity is made persistent, and regenerated every time the entity is modified

@UuidGenerator

UuidGenerator

A more flexible generator for RFC 4122 UUIDs

Furthermore, support for JPA’s standard id generation strategies is also defined in terms of this framework.

As an example, let’s look at how@UuidGenerator is defined:

@IdGeneratorType(org.hibernate.id.uuid.UuidGenerator.class)@ValueGenerationType(generatedBy=org.hibernate.id.uuid.UuidGenerator.class)@Retention(RUNTIME)@Target({FIELD,METHOD})public@interfaceUuidGenerator{...}

@UuidGenerator is meta-annotated both@IdGeneratorType and@ValueGenerationType because it may be used to generate both ids and values of regular attributes.Either way,thisGenerator class does the hard work:

publicclassUuidGenerator// this generator produced values before SQL is executedimplementsBeforeExecutionGenerator{// constructors accept an instance of the @UuidGenerator// annotation, allowing the generator to be "configured"// called to create an id generatorpublicUuidGenerator(org.hibernate.annotations.UuidGeneratorconfig,MemberidMember,GeneratorCreationContextcreationContext){this(config,idMember);}// called to create a generator for a regular attributepublicUuidGenerator(org.hibernate.annotations.UuidGeneratorconfig,Membermember,GeneratorCreationContextcreationContext){this(config,idMember);}...@OverridepublicEnumSet<EventType>getEventTypes(){// UUIDs are only assigned on insert, and never regeneratedreturnINSERT_ONLY;}@OverridepublicObjectgenerate(SharedSessionContractImplementorsession,Objectowner,ObjectcurrentValue,EventTypeeventType){// actually generate a UUID and transform it to the required typereturnvalueTransformer.transform(generator.generateUuid(session));}}

You can find out more about custom generators from the Javadoc for@IdGeneratorType and fororg.hibernate.generator.

9.7. Naming strategies

When working with a pre-existing relational schema, it’s usual to find that the column and table naming conventions used in the schema don’t match Java’s naming conventions.

Of course, the@Table and@Column annotations let us explicitly specify a mapped table or column name.But we would prefer to avoid scattering these annotations across our whole domain model.

Therefore, Hibernate lets us define a mapping between Java naming conventions, and the naming conventions of the relational schema.Such a mapping is called anaming strategy.

First, we need to understand how Hibernate assigns and processes names.

  • Logical naming is the process of applying naming rules to determine thelogical names of objects which were not explicitly assigned names in the O/R mapping.That is, when there’s no@Table or@Column annotation.

  • Physical naming is the process of applying additional rules to transform a logical name into an actual "physical" name that will be used in the database.For example, the rules might include things like using standardized abbreviations, or trimming the length of identifiers.

Thus, there’s two flavors of naming strategy, with slightly different responsibilities.Hibernate comes with default implementations of these interfaces:

FlavorDefault implementation

AnImplicitNamingStrategy is responsible for assigning a logical name when none is specified by an annotation

A default strategy which implements the rules defined by JPA

APhysicalNamingStrategy is responsible for transforming a logical name and producing the name used in the database

A trivial implementation which does no processing

We happen to not much like the naming rules defined by JPA, which specify that mixed case and camel case identifiers should be concatenated using underscores.We bet you could easily come up with a much betterImplicitNamingStrategy than that!(Hint: it should always produce legit mixed case identifiers.)

The popularPhysicalNamingStrategySnakeCaseImpl produces snake case identifiers.

Custom naming strategies may be enabled using the configuration properties we already mentioned without much explanation back inMinimizing repetitive mapping information.

Table 69. Naming strategy configuration
Configuration property namePurpose

hibernate.implicit_naming_strategy

Specifies theImplicitNamingStrategy

hibernate.physical_naming_strategy

Specifies thePhysicalNamingStrategy

9.8. Spatial datatypes

Hibernate Spatial augments thebuilt-in basic types with a set of Java mappings forOGC spatial types.

  • Geolatte-geom defines a set of Java types implementing the OGC spatial types, and codecs for translating to and from database-native spatial datatypes.

  • Hibernate Spatial itself supplies integration with Hibernate.

To use Hibernate Spatial, we must add it as a dependency, as described inOptional dependencies.

Then we may immediately use Geolatte-geom and JTS types in our entities.No special annotations are needed:

importorg.locationtech.jts.geom.Point;importjakarta.persistence.*;@EntityclassEvent{Event(){}Event(Stringname,Pointlocation){this.name=name;this.location=location;}@Id@GeneratedValueLongid;Stringname;Pointlocation;}

The generated DDL usesgeometry as the type of the column mapped bylocation:

createtableEvent(idbigintnotnull,locationgeometry,namevarchar(255),primarykey(id))

Hibernate Spatial lets us work with spatial types just as we would with any of the built-in basic attribute types.

vargeometryFactory=newGeometryFactory();...Pointpoint=geometryFactory.createPoint(newCoordinate(10,5));session.persist(newEvent("Hibernate ORM presentation",point));

But what makes this powerful is that we may write some very fancy queries involving functions of spatial types:

Polygontriangle=geometryFactory.createPolygon(newCoordinate[]{newCoordinate(9,4),newCoordinate(11,4),newCoordinate(11,20),newCoordinate(9,4)});Pointevent=session.createQuery("select location from Event where within(location, :zone) = true",Point.class).setParameter("zone",triangle).getSingleResult();

Here,within() is one of the functions for testing spatial relations defined by the OpenGIS specification.Other such functions includetouches(),intersects(),distance(),boundary(), etc.Not every spatial relation function is supported on every database.A matrix of support for spatial relation functions may be found in theUser Guide.

If you want to play with spatial functions on H2, run the following code first:

sessionFactory.inTransaction(session->{session.doWork(connection->{try(varstatement=connection.createStatement()){statement.execute("create alias if not exists h2gis_spatial for \"org.h2gis.functions.factory.H2GISFunctions.load\"");statement.execute("call h2gis_spatial()");}});});

9.9. Ordered and sorted collections and map keys

Java lists and maps don’t map very naturally to foreign key relationships between tables, and so we tend to avoid using them to represent associations between our entity classes.But if you feel like youreally need a collection with a fancier structure thanSet, Hibernate does have options.

For more detail about the use of these annotations, please refer tothis post on the Hibernate blog.

The following options let us map the index of aList or key of aMap to a column, and are used with:

  • @ElementCollection, or

  • on the owning side of an association.

They should not be used on the unowned (that is,mappedBy) side of an association.

Table 70. Annotations for mapping lists and maps
AnnotationPurposeJPA-standard

@OrderColumn

Specifies the column used to maintain the order of a list

@ListIndexBase

The column value for the first element of the list (zero by default)

@MapKeyColumn

Specifies the column used to persist the keys of a map(used when the key is of basic type)

@MapKeyJoinColumn

Specifies the column used to persist the keys of a map(used when the key is an entity)

The name of the@OrderColumn or@MapKeyColumn may be defaulted, for example:

@ManyToMany@OrderColumn// order of list is persistentList<Author>authors=newArrayList<>();

But it’s usually better to specify the column name explicitly:

@ElementCollection@OrderColumn(name="tag_order")@ListIndexBase(1)// order column and base valueList<String>tags;

Such mappings can get pretty complicated:

@ElementCollection@CollectionTable(name="author_bios",// table namejoinColumns=@JoinColumn(name="book_isbn"))// column holding foreign key of owner@Column(name="bio")// column holding map values@MapKeyJoinColumn(name="author_ssn")// column holding map keysMap<Author,String>biographies;

As you can imagine, we think you should use such mappings very sparingly, if at all.

For aMap representing an unowned@OneToMany association, the column holding the key of the map must also be mapped on the owning side, usually by an attribute of the target entity.In this case we use a different annotation:

Table 71. Annotation for mapping an entity attribute to a map key
AnnotationPurposeJPA-standard

@MapKey

Specifies an attribute of the target entity which acts as the key of the map

Note that@MapKey specifies a field or property name, not a column name.

@OneToMany(mappedBy=Book_.PUBLISHER)@MapKey(name=Book_.TITLE)// the key of the map is the title of the bookMap<String,Book>booksByTitle=newHashMap<>();

In fact,@MapKey may also be used for owned collections.

Now, let’s introduce a little distinction:

  • anordered collection is one with an ordering maintained in the database, and

  • asorted collection is one which is sorted in Java code.

These annotations allow us to specify how the elements of a collection should be ordered as they are read from the database:

Table 72. Annotations for ordered collections
AnnotationPurposeJPA-standard

@OrderBy

Specifies a fragment of JPQL used to order the collection

@SQLOrder

Specifies a fragment of SQL used to order the collection

On the other hand, the following annotations specify how a collection should be sorted in memory, and are used for collections of typeSortedSet orSortedMap:

Table 73. Annotations for sorted collections
AnnotationPurposeJPA-standard

@SortNatural

Specifies that the elements of a collection areComparable

@SortComparator

Specifies aComparator used to sort the collection

Under the covers, Hibernate uses aTreeSet orTreeMap to maintain the collection in sorted order.

9.10. Any mappings

An@Any mapping is a sort of polymorphic many-to-one association where the target entity types are not related by the usual entity inheritance.The target type is distinguished using a discriminator value stored on thereferring side of the relationship.

This is quite different todiscriminated inheritance where the discriminator is held in the tables mapped by the referenced entity hierarchy.

For example, consider anOrder entity containingPayment information, where aPayment might be aCashPayment or aCreditCardPayment:

interfacePayment{...}@EntityclassCashPayment{...}@EntityclassCreditCardPayment{...}

In this example,Payment is not be declared as an entity type, and is not annotated@Entity. It might even be an interface, or at most just a mapped superclass, ofCashPayment andCreditCardPayment. So in terms of the object/relational mappings,CashPayment andCreditCardPayment would not be considered to participate in the same entity inheritance hierarchy.

On the other hand,CashPayment andCreditCardPayment do have the same identifier type.This is important.

An@Any mapping would store the discriminator value identifying the concrete type ofPayment along with the state of the associatedOrder, instead of storing it in the table mapped byPayment.

@EntityclassOrder{...@Any@AnyKeyJavaClass(UUID.class)//the foreign key type@JoinColumn(name="payment_id")// the foreign key column@Column(name="payment_type")// the discriminator column// map from discriminator values to target entity types@AnyDiscriminatorValue(discriminator="CASH",entity=CashPayment.class)@AnyDiscriminatorValue(discriminator="CREDIT",entity=CreditCardPayment.class)Paymentpayment;...}

It’s reasonable to think of the "foreign key" in an@Any mapping as a composite value made up of the foreign key and discriminator taken together. Note, however, that this composite foreign key is only conceptual and cannot be declared as a physical constraint on the relational database table.

There are a number of annotations which are useful to express this sort of complicated and unnatural mapping:

Table 74. Annotations for@Any mappings
AnnotationsPurpose

@Any

Declares that an attribute is a discriminated polymorphic association mapping

@AnyDiscriminator

Specify the Java type of the discriminator

@JdbcType or@JdbcTypeCode

Specify the JDBC type of the discriminator

@AnyDiscriminatorValue

Specifies how discriminator values map to entity types

@Column or@Formula

Specify the column or formula in which the discriminator value is stored

@AnyKeyJavaType or@AnyKeyJavaClass

Specify the Java type of the foreign key (that is, of the ids of the target entities)

@AnyKeyJdbcType or@AnyKeyJdbcTypeCode

Specify the JDBC type of the foreign key

@JoinColumn

Specifies the foreign key column

Of course,@Any mappings are disfavored, except in extremely special cases, since it’s much more difficult to enforce referential integrity at the database level.

There’s also currently some limitations around querying@Any associations in HQL.This is allowed:

fromOrderordjoinCashPaymentcashonid(ord.payment)=cash.id

Polymorphic association joins for@Any mappings are not currently implemented.

Further information may be found in theUser Guide.

9.11. Selective column lists in inserts and updates

By default, Hibernate generatesinsert andupdate statements for each entity during boostrap, and reuses the sameinsert statement every time an instance of the entity is made persistent, and the sameupdate statement every time an instance of the entity is modified.

This means that:

  • if an attribute isnull when the entity is made persistent, its mapped column is redundantly included in the SQLinsert, and

  • worse, if a certain attribute is unmodified when other attributes are changed, the column mapped by that attribute is redundantly included in the SQLupdate.

Most of the time, this just isn’t an issue worth worrying about.The cost of interacting with the database isusually dominated by the cost of a round trip, not by the number of columns in theinsert orupdate.But in cases where it does become important, there are two ways to be more selective about which columns are included in the SQL.

The JPA-standard way is to indicate statically which columns are eligible for inclusion via the@Column annotation.For example, if an entity is always created with an immutablecreationDate, and with nocompletionDate, then we would write:

@Column(updatable=false)LocalDatecreationDate;@Column(insertable=false)LocalDatecompletionDate;

This approach works quite well in many cases, but often breaks down for entities with more than a handful of updatable columns.

An alternative solution is to ask Hibernate to generate SQL dynamically each time aninsert orupdate is executed.We do this by annotating the entity class.

Table 75. Annotations for dynamic SQL generation
AnnotationPurpose

@DynamicInsert

Specifies that aninsert statement should be generated each time an entity is made persistent

@DynamicUpdate

Specifies that anupdate statement should be generated each time an entity is modified

It’s important to realize that, while@DynamicInsert has no impact on semantics, the more useful@DynamicUpdate annotationdoes have a subtle side effect.

The wrinkle is that if an entity has no version property,@DynamicUpdate opens the possibility of two optimistic transactions concurrently reading and selectively updating a given instance of the entity.In principle, this might lead to a row with inconsistent column values after both optimistic transactions commit successfully.

Of course, this consideration doesn’t arise for entities with a@Version attribute.

But there’s a solution!Well-designed relational schemas should haveconstraints to ensure data integrity.That’s true no matter what measures we take to preserve integrity in our program logic.We may ask Hibernate to add acheck constraint to our table using the@Check annotation.Check constraints and foreign key constraints can help ensure that a row never contains inconsistent column values.

9.12. Using the bytecode enhancer

Hibernate’sbytecode enhancer enables the following features:

  • attribute-level lazy fetching for basic attributes annotated@Basic(fetch=LAZY) and for lazy non-polymorphic associations,

  • interception-based—instead of the usualsnapshot-based—detection of modifications.

To use the bytecode enhancer, we must add the Hibernate plugin to our gradle build:

plugins{id"org.hibernate.orm"version"7.0.10.Final"}hibernate{enhancement}

Consider this field:

@EntityclassBook{...@Basic(optional=false,fetch=LAZY)@Column(length=LONG32)StringfullText;...}

ThefullText field maps to aclob ortext column, depending on the SQL dialect.Since it’s expensive to retrieve the full book-length text, we’ve mapped the fieldfetch=LAZY, telling Hibernate not to read the field until it’s actually used.

  • Without the bytecode enhancer, this instruction is ignored, and the field is always fetched immediately, as part of the initialselect that retrieves theBook entity.

  • With bytecode enhancement, Hibernate is able to detect access to the field, and lazy fetching is possible.

By default, Hibernate fetches all lazy fields of a given entity at once, in a singleselect, when any one of them is accessed.Using the@LazyGroup annotation, it’s possible to assign fields to distinct "fetch groups", so that different lazy fields may be fetched independently.

Similarly, interception lets us implement lazy fetching for non-polymorphic associations without the need for a separate proxy object.However, if an association is polymorphic, that is, if the target entity type has subclasses, then a proxy is still required.

Interception-based change detection is a nice performance optimization with a slight cost in terms of correctness.

  • Without the bytecode enhancer, Hibernate keeps a snapshot of the state of each entity after reading from or writing to the database.When the session flushes, the snapshot state is compared to the current state of the entity to determine if the entity has been modified.Maintaining these snapshots does have an impact on performance.

  • With bytecode enhancement, we may avoid this cost by intercepting writes to the field and recording these modifications as they happen.

This optimization isn’tcompletely transparent, however.

Interception-based change detection is less accurate than snapshot-based dirty checking.For example, consider this attribute:

byte[]image;

Interception is able to detect writes to theimage field, that is, replacement of the whole array.It’s not able to detect modifications made directly to theelements of the array, and so such modifications may be lost.

9.13. Named fetch profiles

We’ve already seen two different ways to override the defaultfetching strategy for an association:

  • JPA entity graphs, and

  • thejoin fetch clause inHQL, or, equivalently, the methodFrom.fetch() in the criteria query API.

A third way is to define a named fetch profile.First, we must declare the profile, by annotating a class or package@FetchProfile:

@FetchProfile(name="EagerBook")@EntityclassBook{...}

Note that even though we’ve placed this annotation on theBook entity, a fetch profile—unlike an entity graph—isn’t "rooted" at any particular entity.

We may specify association fetching strategies using thefetchOverrides member of the@FetchProfile annotation, but frankly it looks so messy that we’re embarrassed to show it to you here.

Similarly, a JPAentity graph may be defined using@NamedEntityGraph.But the format of this annotation iseven worse than@FetchProfile(fetchOverrides=…​), so we can’t recommend it. 💀

A better way is to annotate an association with the fetch profiles it should be fetched in:

@FetchProfile(name="EagerBook")@EntityclassBook{...@ManyToOne(fetch=LAZY)@FetchProfileOverride(profile=Book_.PROFILE_EAGER_BOOK,mode=JOIN)Publisherpublisher;@ManyToMany@FetchProfileOverride(profile=Book_.PROFILE_EAGER_BOOK,mode=JOIN)Set<Author>authors;...}
@EntityclassAuthor{...@OneToOne@FetchProfileOverride(profile=Book_.PROFILE_EAGER_BOOK,mode=JOIN)Personperson;...}

Here, once again,Book_.PROFILE_EAGER_BOOK is generated by Hibernate Processor, and is just a constant with the value"EagerBook".

For collections, we may even request subselect fetching:

@FetchProfile(name="EagerBook")@FetchProfile(name="BookWithAuthorsBySubselect")@EntityclassBook{...@OneToOne@FetchProfileOverride(profile=Book_.PROFILE_EAGER_BOOK,mode=JOIN)Personperson;@ManyToMany@FetchProfileOverride(profile=Book_.PROFILE_EAGER_BOOK,mode=JOIN)@FetchProfileOverride(profile=Book_.BOOK_WITH_AUTHORS_BY_SUBSELECT,mode=SUBSELECT)Set<Author>authors;...}

We may define as many different fetch profiles as we like.

Table 76. Annotations for defining fetch profiles
AnnotationPurpose

@FetchProfile

Declares a named fetch profile, optionally including a list of@FetchOverrides

@FetchProfile.FetchOverride

Declares a fetch strategy override as part of the@FetchProfile declaration

@FetchProfileOverride

Specifies the fetch strategy for the annotated association, in a given fetch profile

A fetch profile must be explicitly enabled for a given session by passing the name of the profile toenableFetchProfile():

session.enableFetchProfile(Book_.PROFILE_EAGER_BOOK);BookeagerBook=session.find(Book.class,bookId);

Alternatively, an instance ofEnabledFetchProfile may be obtained in a type safe way from the static metamodel, and used as aFindOption:

BookeagerBook=entityManager.find(Book.class,bookId,Book_._EagerBook);

So why or when might we prefer named fetch profiles to entity graphs?Well, it’s really hard to say.It’s nice that this featureexists, and if you love it, that’s great.But Hibernate offers alternatives that we think are more compelling most of the time.

The one and only advantage unique to fetch profiles is that they let us very selectively requestsubselect fetching.We can’t do that with entity graphs, and we can’t do it with HQL.

There’s a special built-in fetch profile namedorg.hibernate.defaultProfile which is defined as the profile with@FetchProfileOverride(mode=JOIN) applied to every eager@ManyToOne or@OneToOne association.If you enable this profile:

session.enableFetchProfile("org.hibernate.defaultProfile");

Thenouter joins for such associations willautomatically be added to every HQL or criteria query.This is nice if you can’t be bothered typing out thosejoin fetches explicitly.And in principle it even helps partially mitigate theproblem of JPA having specified the wrong default for thefetch member of@ManyToOne.

10. Credits

The full list of contributors to Hibernate ORM can be found on theGitHub repository.

The following contributors were involved in this documentation:

  • Gavin King

Version 7.0.10.Final
Last updated 2025-08-10 00:01:18 UTC

[8]ページ先頭

©2009-2025 Movatter.jp