The best way to map a @NaturalId business key with JPA and Hibernate

Imagine having a tool that can automatically detect if you are using JPA and Hibernate properly. Hypersistence Optimizer is that tool!

Introduction

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.

However, to fetch the entity by its natural key, Hibernate generates the following SQL statements:

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?

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.

FREE EBOOK

4 Comments on “The best way to map a @NaturalId business key with JPA and Hibernate

  1. 2 questions
    First:
    Any way for Hibernate to validate the value of the natural does not exist? I can do it manually but at least when using JPA merge I get a
    update real_estate set author_id=?, building_type=?, city=?, street=? where id=?
    SQL Error: 23505, SQLState: 23505

    Unique index or primary key violation: “UKQPF8S6G3O5ODEXF2FHMN5IK10_INDEX_2 ON PUBLIC.REAL_ESTATE(STREET) VALUES (‘1 scary way’, 7)”; SQL statement:
    update real_estate set author_id=?, building_type=?, city=?, street=? where id=? [23505-197]

    2:
    Seems like @NaturalId does not apply in the case of multi column natural key. Anyway to get hibernate / JPA to magically figure that out so exceptions do not occur when using merge or persist.

    Seems like one needs to use a “upSert” method in a DAO to with at least some user written code to determine if an insert or update should occur and if the natural key would be violated. What I am looking for is a way to push that work to hibernate/JPA

    • I didn’t really understand your questions, and blog comments are not very suitable for answering questions that are not strictly related to the content of the article. I also offer consulting services if you’re interested in hiring me to help you with your JPA or Hibernate issues. Also, for things that don’t take more than 30 minutes, if you buy my High-Performance Java Persistence – Mach 2 video course, I will provide you with a 30-minute consultancy session for free, so seize the deal!

  2. Hi Vlad. Great article.

    I wonder. Shouldn’t you use instanceof operator instead of directly comparing class reference in equals method?

    if (o == null || getClass() != o.getClass())

    • Both work for this example. You would need instanceOf for inheritance or if you pass a proxy.

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.