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:

Post NaturalId

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 20-21 and 23-24 of November about High-Performance Java Persistence.

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.

Transactions and Concurrency Control eBook

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.