@NaturalId – A good way to persist natural IDs with Hibernate?
Take your skills to the next level!
The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.
In the real world, most objects have a natural identifier. Typical examples are the ISBN number of a book, a company’s tax identifier or a person’s social security number. You could, of course, use these identifiers as primary keys. But most often, it’s a better idea to generate numeric, surrogate keys. They are easier to manage, and most frameworks can handle them more efficiently than more complex natural identifiers.
A natural identifier nevertheless identifies a database record and an object in the real world. A lot of use cases use them instead of an artificial, surrogate key. It is, therefore, good practice to model them as unique keys in your database. Hibernate also allows you to model them as a natural identifier of an entity and provides an extra API for retrieving them from the database.
Define an attribute as a natural id
The only thing you have to do to model an attribute is a natural id, is to add the @NaturalId annotation. You can see an example in the following code snippet. The isbn number of a Book is a typical natural id. It identifies the record but is more complex than the primary key id. The id attribute is a surrogate key and gets generated by Hibernate.
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NaturalId
private String isbn;
…
}
Natural IDs are immutable by default and you should not provide setter methods for them. If you need mutable, natural identifier, you have to set the mutable attribute of the @NaturalId annotation to true.
Get an entity by its natural id
Hibernate’s Session interface provides the methods byNaturalId and bySimpleNaturalId to read an entity by its natural identifier from the database. Let’s have a look at the byNaturalId method first.
The following code snippet shows how you can use this method to get an entity by its natural ID. You have to provide the class or the name of the entity as a parameter to the byNaturalId method.
The call of the using method provides the name of the natural ID attribute and its value. If the natural ID consists of multiple attributes, you have to call this method multiple times to define each part of the ID. In this example, I use the JPA metamodel to get the name of the isbn attribute.
After you’ve provided the value of the natural id, you can call the load method to get the entity identified by it. Hibernate also offers other options to get the entity which I show you in the following section.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book b = session.byNaturalId(Book.class).using(Book_.isbn.getName(), "978-0321356680").load();
One thing that surprised me when I used this API for the first time was the number of queries Hibernate performs. I expected, that Hibernate creates 1 SQL statement to read the entity. But that’s not the case. Hibernate performs 2 queries, as you can see in log messages below. The first query selects the primary for the given natural Id and the second one uses it to get the entity.
The reason for this approach is most likely that Hibernate needs the primary key value internally to check the 1st and 2nd level cache. In most cases, this additional query should not have a huge impact on the performance. Hibernate also caches the natural id to primary key mapping for the session and can store it in the 2nd level cache so that there is no need to retrieve it again.
06:14:40,705 DEBUG SQL:92 – select book_.id as id1_0_ from Book book_ where book_.isbn=?
06:14:40,715 DEBUG SQL:92 – select book0_.id as id1_0_0_, book0_.isbn as isbn2_0_0_, book0_.publishingDate as publishi3_0_0_, book0_.title as title4_0_0_, book0_.version as version5_0_0_ from Book book0_ where book0_.id=?
The bySimpleNaturalId method provides a convenient option to select entities with simple natural IDs that consist of only one attribute. As you can see in the following code snippet, you can provide the natural ID value directly to the load method and don’t need to call the using method.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book b = session.bySimpleNaturalId(Book.class).load("978-0321356680");
3 Options to retrieve the entity
As I explained earlier, Hibernate offers 3 different options to retrieve an entity by its natural ID from the database:
load() | Gets a reference to the initialized entity. |
loadOptional() | Gets a reference to the initialized entity or null and wraps it into an Optional. I explained Hibernate’s Optional support in more detail in How to use Java 8’s Optional with Hibernate. |
getReference() | Gets a reference to the entity or an uninitialized proxy. |
Locking
The interfaces NaturalIdLoadAccess and SimpleNaturalIdLoadAccess provide the with(LockOptions lock) method. You probably know it from the IdentifierLoadAccess interface which gets returned by the Session.byId(Class entity) method. You can use this method to define which lock mode Hibernate shall use for the query.
In the following code snippet, I use this method to set a write lock on the selected entity.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
Book b = session.bySimpleNaturalId(Book.class).with(LockOptions.UPGRADE).load("978-0321356680");
You can see in the logged SQL statement, that Hibernate added “for update” to the query. This keyword triggers the write lock in the PostgreSQL database I use for this example.
06:19:34,055 DEBUG SQL:92 – select book_.id as id1_0_ from Book book_ where book_.isbn=?
06:19:34,128 DEBUG SQL:92 – select book0_.id as id1_0_0_, book0_.isbn as isbn2_0_0_, book0_.publishingDate as publishi3_0_0_, book0_.title as title4_0_0_, book0_.version as version5_0_0_ from Book book0_ where book0_.id=? for update
Caching
As I explained in the beginning, Hibernate caches the natural id to primary key mapping for each session. You can see an example of it in the following code snippet and the corresponding log messages.
I first load the Book entity with id 1 from the database and write a log message. In the next step, I load the same entity by its natural identifier.
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);
session.byId(Book.class).load(1L);
log.info(“Get book by id”);
Book b = session.bySimpleNaturalId(Book.class).load("978-0321356680");
As you can see in the log messages, Hibernate performs a select statement to get the Book entity with id 1. But it doesn’t execute another statement to get it by its natural ID. Hibernate added the primary key to natural ID mapping to the session when I loaded the entity by its id. When I then load the entity by its natural ID, Hibernate gets the primary key mapping and the entity from the 1st level cache.
06:20:39,767 DEBUG SQL:92 – select book0_.id as id1_0_0_, book0_.isbn as isbn2_0_0_, book0_.publishingDate as publishi3_0_0_, book0_.title as title4_0_0_, book0_.version as version5_0_0_ from Book book0_ where book0_.id=?
06:20:39,785 INFO TestHibernateNaturalId:78 – Read book by id
06:20:39,788 INFO TestHibernateNaturalId:81 – Book title: Effective Java
Conclusion
Selecting entities by its natural identifier is a common use case. Hibernate’s proprietary API provides an easy and comfortable way to do that. The additional select statement to get the primary key for the provided natural ID comes as a surprise in the beginning. But this should not be a performance issue, if you consider, that you normally add a database index to your natural identifier column. As soon as Hibernate knows the mapping between the natural id and the primary key, it can use the known optimization and caching mechanisms.