The best way to map a @NaturalId business key with JPA and Hibernate
Imagine having a tool that can automatically detect JPA and Hibernate performance issues. Wouldn’t that be just awesome?
Well, Hypersistence Optimizer is that tool! And it works with Spring Boot, Spring Framework, Jakarta EE, Java EE, Quarkus, or Play Framework.
So, enjoy spending your time on the things you love rather than fixing performance issues in your production system on a Saturday night!
Introduction
In this article, you are going to learn what the Hibernate natural id is and how you can use it to fetch entities based on a business key.
As I explained in this free chapter of my book, Hibernate offers many benefits over standard JPA. One such example is the @NaturalId
mapping.
In this article, you are going to see what is the best way to map a natural business key when using Hibernate.
Domain Model
Considering we have the following Post
entity:
The slug
attribute is the business key for our Post
entity. As I explained previously, we use a surrogate key as well because it’s much more compact and it puts less pressure on memory for both table and index pages.
The id
property, being the entity identifier, can be marked with the JPA @Id
annotation, but for the slug
attribute, we need a Hibernate-specific annotation: @NaturalId
.
@Entity(name = "Post") @Table(name = "post") public class Post { @Id @GeneratedValue private Long id; private String title; @NaturalId @Column(nullable = false, unique = true) private String slug; //Getters and setters omitted for brevity @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Post post = (Post) o; return Objects.equals(slug, post.slug); } @Override public int hashCode() { return Objects.hash(slug); } }
As I explained previously, implementing equals
and hashCode
is straightforward when the entity defines a natural identifier.
If the entity does not define a natural identifier, implementing equals
and hashCode
should be done as I explained in this article.
Natural id fetching
Hibernate allows you to fetch entities either directly, via the entity identifier, or through a JPQL or SQL query.
Just like with the JPA @Id
annotation, the @NaturalId
allows you to fetch the entity if you know the associated natural key.
So, considering you have the following Post
entity:
Post post = new Post(); post.setTitle("High-Performance Java persistence"); post.setSlug("high-performance-java-persistence"); entityManager.persist(post);
Knowing the natural key, you can now fetch the Post
entity as follows:
String slug = "high-performance-java-persistence"; Post post = entityManager.unwrap(Session.class) .bySimpleNaturalId(Post.class) .load(slug);
If you have a single @NaturalId
attribute defined in your entity, you should always use the bySimpleNaturalId
method.
However, in case you have a compound @NaturalId
, meaning that you declared more than one @NaturalId
properties, then you need to use the byNaturalId
method instead:
Post post = entityManager.unwrap(Session.class) .byNaturalId(Post.class) .using("slug", slug) .load();
That’s great because the slug
attribute is what the client will see in the browser address bar. Since the post URL can be bookmarked, we can now load the Post
by the slug
attribute sent by the client.
Hibernate 5.5 or newer
When fetching the entity by its natural key on Hibernate 5.5 or newer, the following SQL query is generated:
SELECT p.id AS id1_0_0_, p.slug AS slug2_0_0_, p.title AS title3_0_0_ FROM post p WHERE p.slug = 'high-performance-java-persistence'
So, since Hibernate 5.5, the entity is fetched by its natural identifier directly from the database.
Hibernate 5.4 or older
When fetching the entity by its natural key on Hibernate 5.4 or older, two SQL queries are generated:
SELECT p.id AS id1_0_ FROM post p WHERE p.slug = 'high-performance-java-persistence' SELECT p.id AS id1_0_0_, p.slug AS slug2_0_0_, p.title AS title3_0_0_ FROM post p WHERE p.id = 1
The first query is needed to resolve the entity identifier associated with the provided natural identifier.
The second query is optional if the entity is already loaded in the first or the second-level cache.
The reason for having the first query is because Hibernate already has a well-established logic for loading and associating entities by their identifier in the Persistence Context.
Optimizing the entity identifier retrieval
Just like you can avoid hitting the database to fetch an entity, you can skip the entity identifier retrieval by its associated natural key using the Hibernate @NaturalIdCache
:
@Entity(name = "Post") @Table(name = "post") @org.hibernate.annotations.Cache( usage = CacheConcurrencyStrategy.READ_WRITE ) @NaturalIdCache public class Post { @Id @GeneratedValue private Long id; private String title; @NaturalId @Column(nullable = false, unique = true) private String slug; //Getters and setters omitted for brevity @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Post post = (Post) o; return Objects.equals(slug, post.slug); } @Override public int hashCode() { return Objects.hash(slug); } }
We also annotated the entity using the Hibernate-specific @Cache
annotation so that we declare a READ_WRITE
Cache Concurrency Strategy.
This time, when running the previous example and fetch the Post
entity, Hibernate generates zero SQL statements.
Because the READ_WRITE
Cache Concurrency Strategy is write-through, the Post
entity is cached during the persist
operation, along with the natural key to identifier mapping.
If we were using NONSTRICT_READ_WRITE
Cache Concurrency Strategy, the Post
entity would be cached upon being accessed for the very first time.
However, for READ_WRITE
, we don’t have to hit the database at all when fetching our Post
entity. Cool, right?
I'm running an online workshop on the 11th of October about High-Performance SQL.If you enjoyed this article, I bet you are going to love my Book and Video Courses as well.
Conclusion
The @NaturalId
annotation is a very useful Hibernate feature that allows you to retrieve entities by their natural business key without even hitting the database.
