What is a natural identifier in Hibernate?
In Hibernate, natural keys are often used for lookups. You will have an auto-generated surrogate id in most cases. But this id is rather useless for lookups, as you'll always query by fields like name, social security number or anything else from the real world.
When using Hibernate's caching features, this difference is very important: If the cache is indexed by your primary key (surrogate id), there won't be any performance gain on lookups. That's why you can define a set of fields that you are going to query the database with - the natural id. Hibernate can then index the data by your natural key and improve the lookup performance.
See this excellent blog post for a more detailed explanation or this RedHat page for an example Hibernate mapping file.
In a relational database system, typically, you can have two types of simple identifiers:
- Natural keys, which are assigned by external systems and guaranteed to be unique
- Surrogate keys, like
IDENTITY
orSEQUENCE
which are assigned by the database.
The reason why Surrogate Keys are so popular is that they are more compact (4 bytes or 8 bytes), compared to a Natural Key which is very long (e.g. the VIN takes 17 alphanumerical characters, the book ISBN is 13 digits long). If the Surrogate Key becomes the Primary Key, you can map it using the JPA @Id
annotation.
Now, let's assume we have the following Post
entity:
Since the Post
entity that has also a Natural Key, besides the Surrogate one, you can map it with the Hibernate-specific @NaturalId
annotation:
@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);
}
}
Now, considering the entity above, the user might have bookmarked a Post
article and now they want to read it. However, the bookmarked URL contains the slug
Natural Identifier, not the Primary Key.
So, we can fetch it like this using Hibernate:
Post post = entityManager.unwrap(Session.class)
.bySimpleNaturalId(Post.class)
.load(slug);
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.
Now, if you want to skip the entity identifier query, you can easily annotate the entity using the @NaturalIdCache
annotation:
@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);
}
}
This way, you can fetch the Post
entity without even hitting the database. Cool, right?