5 Primary Key Mappings for JPA and Hibernate Every Developer Should Know


Take your skills to the next level!

The Persistence Hub is the place to be for every Java developer. It gives you access to all my premium video courses, monthly Java Persistence News, monthly coding problems, and regular expert sessions.


Mapping a primary key column with JPA and Hibernate is simple. You just need to add an attribute to your entity, make sure that its type and name match the database column, annotate it with @Column and you’re done. You can then use the primary key to load the entity, and Hibernate sets the primary key value automatically. And if you want to persist a new entity, you need to set the primary key value programmatically.

But with JPA and Hibernate you can do much more than that. You can:

  • choose between different strategies to generate unique primary key values,
  • use UUIDs and generate their values,
  • map composite primary keys,
  • share primary key values across associations and
  • map natural IDs.

Generate Numeric Primary Key Values

Most table models use simple, numerical primary keys. They are easy to use and very efficient at the same time.

You can either set their values programmatically or use one of JPA’s generation strategies to create them automatically. The easiest way to do that is to annotate your primary key attribute with a @GeneratedValue annotation. Hibernate will then pick a strategy based on the database-specific dialect.

@Entity
public class Book {
 
    @Id
    @GeneratedValue
    private Long id;
     
    …
}

Using the auto strategy, or not referencing a strategy at all, is the simplest but not the best way. It’s better to specify the strategy. You can choose between:

  • GenerationType.AUTO – Let Hibernate pick one of the following strategies.
  • GenerationType.SEQUENCE – Use a database sequence.
  • GenerationType.IDENTITY – Use an autoincremented database columns.
  • GenerationType.TABLE – Use a database table to simulate a sequence.

That ensures that a Hibernate update will not accidentally change your generation strategy and if you’re using the GenerationType.SEQUENCE, it will also activate Hibernate’s performance optimizations.

Defining the strategy is simple. You just need to provide it as the value of the strategy attribute of the @GeneratedValue annotation.

The following mapping tells Hibernate to use a database sequence to generate primary key values.

@Entity
public class Book {
 
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private Long id;
     
    …
}

By default, Hibernate uses a sequence called hibernate_sequence. You can also tell Hibernate to use one of your own database sequences. I explained that in more details in Hibernate Tips: How to use a custom database sequence.

Generate UUID Primary Keys

UUIDs and numerical primary keys might seem very different. But with Hibernate, you can map and use them in almost the same way. The only difference is the type of the primary key attribute, which is a java.util.UUID instead of a java.lang.Long.

Here is a simple example. The Book entity maps an attribute of type UUID and uses one of Hibernate’s generators to create primary key values automatically before persisting a new entity.

@Entity
public class Book {

	@Id
	@GeneratedValue
	private UUID id;
	
	…
}

This is the easiest way to map and generate a UUID as a primary key. If you want to take a more detailed look at your mapping options, please read How to generate UUIDs as primary keys with Hibernate.

You can then use this Book entity in the same way as you would use an entity that maps a primary key attribute of type Long.

Book b = new Book();
b.setTitle(“Hibernate Tips - More than 70 solutions to common Hibernate problems”);
b.setPublishingDate(LocalDate.of(2017, 4, 4));
em.persist(b);

When Hibernate persists this Book entity, it first generates a UUID. It then sets that value as the id value in the SQL INSERT statement. You can see this in the log file if you activate my recommended development configuration.

12:23:19,356 DEBUG AbstractSaveEventListener:118 – Generated identifier: d7cd23b8-991c-470f-ac63-d8fb106f391e, using strategy: org.hibernate.id.UUIDGenerator
12:23:19,388 DEBUG SQL:92 – insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
12:23:19,392 TRACE BasicBinder:65 – binding parameter [1] as [DATE] – [2017-04-04]
12:23:19,393 TRACE BasicBinder:65 – binding parameter [2] as [VARCHAR] – [Hibernate Tips - More than 70 solutions to common Hibernate problems]
12:23:19,393 TRACE BasicBinder:65 – binding parameter [3] as [INTEGER] – [0]
12:23:19,394 TRACE BasicBinder:65 – binding parameter [4] as [OTHER] – [d7cd23b8-991c-470f-ac63-d8fb106f391e]

Manage Composite Primary Keys

JPA and Hibernate also provide multiple ways to map composite primary keys that consist of multiple attributes. Let’s take a look at my preferred option: the embedded id.

I explain this and all other options in great details in my Advanced Hibernate Online Training (enrollment opens next week).

The embedded id approach uses an embeddable to map the primary key attributes.

An embeddable is a pure Java class that is annotated with @Embeddable. It defines attribute mappings in a reusable way.

If you want to use it as an embedded id, you also need to implement the equals and hashCode methods.

@Embeddable
public class AddressKey implements Serializable {
 
    private Long xId;
    private Long yId;
     
    public AddressKey() {}
     
    public AddressKey(Long xId, Long yId) {
        super();
        this.xId = xId;
        this.yId = yId;
    }
 
    public Long getxId() {
        return xId;
    }
 
    public void setxId(Long xId) {
        this.xId = xId;
    }
 
    public Long getyId() {
        return yId;
    }
 
    public void setyId(Long yId) {
        this.yId = yId;
    }
 
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((xId == null) ? 0 : xId.hashCode());
        result = prime * result + ((yId == null) ? 0 : yId.hashCode());
        return result;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        AddressKey other = (AddressKey) obj;
        if (xId == null) {
            if (other.xId != null)
                return false;
        } else if (!xId.equals(other.xId))
            return false;
        if (yId == null) {
            if (other.yId != null)
                return false;
        } else if (!yId.equals(other.yId))
            return false;
        return true;
    }
}

You can then use the embeddable class as the type of your primary key attribute and annotate it with @EmbeddedId. The embeddable and all its attributes become part of the entity. It follows the same lifecycle, and all its attributes get mapped to the database table that’s mapped by the entity.

@Entity
public class Address {
 
    @EmbeddedId
    private AddressKey id;
 
    private String city;
 
    private String street;
 
    private String country;
 
    @OneToOne(mappedBy = "address")
    private Person person;
 
    ...
}

After you defined the mapping, you can easily use the embedded id to create a new or to fetch an existing entity.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();

Address a = new Address();
AddressKey aKey = new AddressKey(1L, 2L);
a.setId(aKey);
a.setCity("A City");
a.setCountry("A Country");
a.setStreet("A Street");
em.persist(a);

em.getTransaction().commit();
em.close();

em = emf.createEntityManager();
em.getTransaction().begin();

aKey = new AddressKey(1L, 2L);
a = em.find(Address.class, aKey);

em.getTransaction().commit();
em.close();
13:30:30,824 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        Address
        (city, country, street, xId, yId) 
    values
        (?, ?, ?, ?, ?)
13:30:30,865 DEBUG [org.hibernate.SQL] - 
    select
        address0_.xId as xId1_0_0_,
        address0_.yId as yId2_0_0_,
        address0_.city as city3_0_0_,
        address0_.country as country4_0_0_,
        address0_.street as street5_0_0_ 
    from
        Address address0_ 
    where
        address0_.xId=? 
        and address0_.yId=?

Use Same Primary Key Values for Associated Entities

Another common primary key mapping is to use the same primary key value in a one-to-one association.

You can, of course, map this with JPA and Hibernate. The only things you need to do are to model the owning side of the association on the entity that shall reuse the primary key value and to add a @MapsId annotation to it.

@Entity
public class Manuscript {
 
    @Id
    private Long id;
     
    private byte[] file;
     
    @OneToOne
    @JoinColumn(name = "id")
    @MapsId
    private Book book;
     
    ...
}

When you persist the Manuscript entity, you only need to set the association to the Book entity. Hibernate will then use the primary key value of the Book for the new Manuscript.

Book b = em.find(Book.class, 1L);
         
Manuscript m = new Manuscript();
m.setBook(b);
 
b.setManuscript(m);
 
em.persist(m);

You can dive deeper into this mapping in Hibernate Tips: How to Share the Primary Key in a One-to-One Association.

Work with Natural ID

Most teams prefer to use a surrogate key as the primary key. It’s easier to manage in your code, and all involved systems can handle it more efficiently. But modeling a natural ID is still useful. You will, most likely, reference them very often in your use cases.

Hibernate provides an annotation to declare a natural ID and an API to retrieve entities by it. Let’s take a quick look at the most important details. And if you want to dive deeper, please read my article @NaturalId – A good way to persist natural IDs with Hibernate?

You can specify a natural ID by annotating one or more entity attributes with @NaturalId. I use it in the following code snippet, to tell Hibernate that the isbn attribute is a natural ID of the Book entity.

Entity
public class Book {

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  private Long id;

  @NaturalId
  private String isbn;

  …
}

After you’ve done that, you can use the byNaturalId method on Hibernate’s Session interface to create a query that loads an entity by its natural id. If you’re using JPA’s EntityManager, you can get the corresponding Session interface by calling the unwrap method.

In the next step, you need to provide the value of the natural id by calling the using method for each attribute that’s part of the natural id. In this example, the natural id only consists of the isbn attribute, which I reference using the JPA metamodel class of the Book entity.

And after you provided the natural id value, you can call the load method to execute the query.

EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Session session = em.unwrap(Session.class);

Book b = session.byNaturalId(Book.class).using(Book_.isbn.getName(), “978-0321356680”).load();

When you run this code and activate the logging of SQL statements, you can see that Hibernate first gets the primary key value for the provided natural id. It then executes a second query to load the entity by its primary key. The result of the first query gets cached so that Hibernate doesn’t need to perform it again.

06:14:40,705 DEBUG SQL:92 – select book_.id as id1_0_ from Book book_ where book_.isbn=?
06:14:40,715 DEBUG SQL:92 – select book0_.id as id1_0_0_, book0_.isbn as isbn2_0_0_, book0_.publishingDate as publishi3_0_0_, book0_.title as title4_0_0_, book0_.version as version5_0_0_ from Book book0_ where book0_.id=?

Conclusion

JPA and Hibernate can do much more than just mapping a numerical primary key column to an entity attribute. You can use them to generate unique primary key values, to map and create UUIDs, to work with composite primary keys, and to use the same primary key value for associated entities. And Hibernate also supports natural primary keys with its own, proprietary query mechanism.

4 Comments

  1. Hi,

    I need to find the correct annotation for Hibernate to generate a DDL for Oracle id column like this:
    "GENERATED BY DEFAULT ON NULL AS IDENTITY"

    1. Avatar photo Thorben Janssen says:

      That should be
      @Id
      @GeneratedValue(strategy = GenerationType.SEQUENCE)
      private Long id;

      I explained that in more details here.

  2. Hi,

    UUID as primary key is not a good idea. UUIDs are very very slow and they are huge compared to a Long. An UUID can waste more space on the hard disk than the rest of the table row.

    Greetings

    1. Avatar photo Thorben Janssen says:

      Hi Aleks,

      That are good reasons against using UUIDs as primary keys. And I agree with you, that a simple number, e.g., a Long, is more efficient.
      But UUIDs have one advantage: You don’t need a single source of truth, e.g., a database sequence, to get a new primary key value in a distributed environment. That is essential if you need to support:

      • offline clients
      • server in different data centers which need to be able to operate independently of each other and to also join their data
      • aggregate data from multiple, independent services

      Regards,
      Thorben

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.