Natural Ids

Natural ids represent domain model unique identifiers that have a meaning in the real world too. Even if a natural id does not make a good primary key (surrogate keys being usually preferred), it’s still useful to tell Hibernate about it. As we will see later, Hibernate provides a dedicated, efficient API for loading an entity by its natural id much like it offers for loading by its identifier (PK).

Natural Id Mapping

Natural ids are defined in terms of on e or more persistent attributes.

Example 1. Natural id using single basic attribute
@Entity(name = "Book")
public static class Book {

    @Id
    private Long id;

    private String title;

    private String author;

    @NaturalId
    private String isbn;

    //Getters and setters are omitted for brevity
}
Example 2. Natural id using single embedded attribute
@Entity(name = "Book")
public static class Book {

    @Id
    private Long id;

    private String title;

    private String author;

    @NaturalId
    @Embedded
    private Isbn isbn;

    //Getters and setters are omitted for brevity
}

@Embeddable
public static class Isbn implements Serializable {

    private String isbn10;

    private String isbn13;

    //Getters and setters are omitted for brevity

    @Override
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }
        Isbn isbn = (Isbn) o;
        return Objects.equals( isbn10, isbn.isbn10 ) &&
                Objects.equals( isbn13, isbn.isbn13 );
    }

    @Override
    public int hashCode() {
        return Objects.hash( isbn10, isbn13 );
    }
}
Example 3. Natural id using multiple persistent attributes
@Entity(name = "Book")
public static class Book {

    @Id
    private Long id;

    private String title;

    private String author;

    @NaturalId
    private String productNumber;

    @NaturalId
    @ManyToOne(fetch = FetchType.LAZY)
    private Publisher publisher;

    //Getters and setters are omitted for brevity
}

@Entity(name = "Publisher")
public static class Publisher implements Serializable {

    @Id
    private Long id;

    private String name;

    //Getters and setters are omitted for brevity

    @Override
    public boolean equals(Object o) {
        if ( this == o ) {
            return true;
        }
        if ( o == null || getClass() != o.getClass() ) {
            return false;
        }
        Publisher publisher = (Publisher) o;
        return Objects.equals( id, publisher.id ) &&
                Objects.equals( name, publisher.name );
    }

    @Override
    public int hashCode() {
        return Objects.hash( id, name );
    }
}

Natural Id API

As stated before, Hibernate provides an API for loading entities by their associate natural id. This is represented by the org.hibernate.NaturalIdLoadAccess contract obtained via Session#byNaturalId.

If the entity does not define a natural id, trying to load an entity by its natural id will throw an exception.

Example 4. Using NaturalIdLoadAccess
Book book = entityManager
    .unwrap(Session.class)
    .byNaturalId( Book.class )
    .using( "isbn", "978-9730228236" )
    .load();
Book book = entityManager
    .unwrap(Session.class)
    .byNaturalId( Book.class )
    .using(
        "isbn",
        new Isbn(
            "973022823X",
            "978-9730228236"
        ) )
    .load();
Book book = entityManager
    .unwrap(Session.class)
    .byNaturalId( Book.class )
    .using("productNumber", "973022823X")
    .using("publisher", publisher)
    .load();

NaturalIdLoadAccess offers 2 distinct methods for obtaining the entity:

load()

obtains a reference to the entity, making sure that the entity state is initialized

getReference()

obtains a reference to the entity. The state may or may not be initialized. If the entity is already associated with the current running Session, that reference (loaded or not) is returned. If the entity is not loaded in the current Session and the entity supports proxy generation, an uninitialized proxy is generated and returned, otherwise the entity is loaded from the database and returned.

NaturalIdLoadAccess allows loading an entity by natural id and at the same time apply a pessimistic lock. For additional details on locking, see the Locking chapter.

We will discuss the last method available on NaturalIdLoadAccess ( setSynchronizationEnabled() ) in Natural Id - Mutability and Caching.

Because the Company and PostalCarrier entities define "simple" natural ids, we can load them as follows:

Example 5. Loading by simple natural id
Book book = entityManager
    .unwrap(Session.class)
    .bySimpleNaturalId( Book.class )
    .load( "978-9730228236" );
Book book = entityManager
    .unwrap(Session.class)
    .bySimpleNaturalId( Book.class )
    .load(
        new Isbn(
            "973022823X",
            "978-9730228236"
        )
    );

Here we see the use of the org.hibernate.SimpleNaturalIdLoadAccess contract, obtained via `Session#bySimpleNaturalId().

SimpleNaturalIdLoadAccess is similar to NaturalIdLoadAccess except that it does not define the using method. Instead, because these simple natural ids are defined based on just one attribute we can directly pass the corresponding natural id attribute value directly to the load() and getReference() methods.

If the entity does not define a natural id, or if the natural id is not of a "simple" type, an exception will be thrown there.

Natural Id - Mutability and Caching

A natural id may be mutable or immutable. By default the @NaturalId annotation marks an immutable natural id attribute. An immutable natural id is expected to never change its value.

If the value(s) of the natural id attribute(s) change, @NaturalId(mutable=true) should be used instead.

Example 6. Mutable natural id mapping
@Entity(name = "Author")
public static class Author {

    @Id
    private Long id;

    private String name;

    @NaturalId(mutable = true)
    private String email;

    //Getters and setters are omitted for brevity
}

Within the Session, Hibernate maintains a mapping from natural id values to entity identifiers (PK) values. If natural ids values changed, it is possible for this mapping to become out of date until a flush occurs.

To work around this condition, Hibernate will attempt to discover any such pending changes and adjust them when the load() or getReference() methods are executed. To be clear: this is only pertinent for mutable natural ids.

This discovery and adjustment have a performance impact. If an application is certain that none of its mutable natural ids already associated with the Session have changed, it can disable that checking by calling setSynchronizationEnabled(false) (the default is true). This will force Hibernate to circumvent the checking of mutable natural ids.

Example 7. Mutable natural id synchronization use-case
Author author = entityManager
    .unwrap(Session.class)
    .bySimpleNaturalId( Author.class )
    .load( "john@acme.com" );

author.setEmail( "john.doe@acme.com" );

assertNull(
    entityManager
        .unwrap(Session.class)
        .bySimpleNaturalId( Author.class )
        .setSynchronizationEnabled( false )
        .load( "john.doe@acme.com" )
);

assertSame( author,
    entityManager
        .unwrap(Session.class)
        .bySimpleNaturalId( Author.class )
        .setSynchronizationEnabled( true )
        .load( "john.doe@acme.com" )
);

Not only can this NaturalId-to-PK resolution be cached in the Session, but we can also have it cached in the second-level cache if second level caching is enabled.

Example 8. Natural id caching
@Entity(name = "Book")
@NaturalIdCache
public static class Book {

    @Id
    private Long id;

    private String title;

    private String author;

    @NaturalId
    private String isbn;

    //Getters and setters are omitted for brevity
}