Most developers prefer numerical primary keys because they are efficient to use and easy to generate. But that doesn’t mean that a primary key has to be a number.
UUIDs, for example, have gained some popularity over recent years. The main advantage of a UUID is its (practical) global uniqueness which provides a huge advantage for distributed systems.
If you use the typical numerical ID that gets incremented for each new record, you need to generate all IDs by the same system component. In most cases, this is a sequence for each table that’s managed by your database. This makes that sequence the single source of failure. Other approaches, e.g., a clustered database or any other horizontally scaled number generator, require communication between the nodes. That obviously creates some effort that slows down the generation of your primary key values.
You don’t need any of this when using a globally unique UUID. Each component can generate its own UUIDs, and there will not be any conflicts. That’s why UUIDs have become popular in microservice-based architectures or when developing offline clients.
On the other hand, the UUID also has some disadvantages. The most obvious one is its size. It’s 4 times larger than a numerical ID and can’t be handled as efficiently. You should, therefore, decide carefully if you want to use UUIDs or numeric IDs and discuss it with your database administrator.
If you decide to use UUIDs, you can, of course, also persist them with Hibernate. When doing that, you need to decide how you want to generate the UUID value. You can, of course, generate it yourself and set it on your entity object before persisting it. Or, if you’re using Hibernate 4, 5, or 6 or JPA 3.1, you can define a generation strategy in your entity mappings. I will show you how to do that in this article.
Since JPA 3.1, you can annotate a primary key attribute with @GeneratedValue and set the strategy to GenerationType.UUID. Based on the specification, your persistence provider shall generate a UUID value based on IETF RFC 4122.
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
private UUID id;
…
}
Let’s try this mapping and persist a new Book entity object.
Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);
You can see in the log output that Hibernate generated a UUID value and set it on the Book entity object before it persisted it in the database.
18:27:50,009 DEBUG AbstractSaveEventListener:127 - Generated identifier: 21e22474-d31f-4119-8478-d9d448727cfe, using strategy: org.hibernate.id.UUIDGenerator
18:27:50,035 DEBUG SQL:128 - insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
18:27:50,039 TRACE bind:28 - binding parameter [1] as [DATE] - [1902-04-30]
18:27:50,040 TRACE bind:28 - binding parameter [2] as [VARCHAR] - [The Hound of the Baskervilles]
18:27:50,040 TRACE bind:28 - binding parameter [3] as [INTEGER] - [0]
18:27:50,040 TRACE bind:28 - binding parameter [4] as [BINARY] - [21e22474-d31f-4119-8478-d9d448727cfe]
IETF RFC 4122 defines 4 different strategies to generate UUIDs. But unfortunately, JPA 3.1 doesn’t specify which version your persistence provider shall use. It also doesn’t define any portable mechanism to customize this generation process.
Due to that, your persistence provider can decide how it generates UUID values. And this behavior might differ between JPA implementations.
When you use Hibernate as your persistence provider, it generates the UUID value based on random numbers, as defined by IETF RFC 4122 Version 4. I get into more details about that when I show you Hibernate’s proprietary UUID generators.
As mentioned earlier, IETF RFC 4122 defines 4 different strategies to generate UUIDs. Hibernate supports 2 of them:
The definition of the strategy you want to use depends on your Hibernate version. Let’s have a look at the default strategy first.
By default, Hibernate uses a random number based generation strategy. This is also the strategy that Hibernate uses if you use the previously described, JPA-based definition.
Using Hibernate 6, you can annotate your primary key attribute with @UuidGenerator and set the style to RANDOM, AUTO, or don’t specify it. In all 3 cases, Hibernate will apply its default strategy.
@Entity
public class Book {
@Id
@GeneratedValue
@UuidGenerator
private UUID id;
...
}
Let’s use this mapping with the test as I showed you before.
Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);
Unsurprisingly, this gets you the same log output as in the previous test case. Internally, Hibernate used the same style when I used JPA’s mapping annotations.
18:28:25,859 DEBUG AbstractSaveEventListener:127 - Generated identifier: ac864ed4-bd3d-4ca0-8ba2-b49ec74465ff, using strategy: org.hibernate.id.uuid.UuidGenerator
18:28:25,879 DEBUG SQL:128 - insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
18:28:25,886 TRACE bind:28 - binding parameter [1] as [DATE] - [1902-04-30]
18:28:25,887 TRACE bind:28 - binding parameter [2] as [VARCHAR] - [The Hound of the Baskervilles]
18:28:25,887 TRACE bind:28 - binding parameter [3] as [INTEGER] - [0]
18:28:25,888 TRACE bind:28 - binding parameter [4] as [BINARY] - [ac864ed4-bd3d-4ca0-8ba2-b49ec74465ff]
If you’re using Hibernate 4 or 5, you can use the same feature. But you need to put a little extra effort into your mapping definition.
You need to annotate your primary key attribute with a @GeneratedValue annotation. In that annotation, you need to reference a custom generator and define that generator using Hibernate’s @GenericGenerator annotation. The @GenericGenerator annotation requires 2 parameters, the name of the generator and the name of the class that implements the generator. In this case, I called the generator “UUID” and Hibernate shall use the class org.hibernate.id.UUIDGenerator.
@Entity
public class Book {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator"
)
private UUID id;
…
}
That’s all you need to do to tell Hibernate to generate a UUID as a primary key. Let’s use this mapping to persist a new Book entity object.
Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);
As you can see in the log output, Hibernate generates a UUID and sets it as the id value before it writes the new record to the database.
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] – [1902-04-30]
12:23:19,393 TRACE BasicBinder:65 – binding parameter [2] as [VARCHAR] – [The Hound of the Baskervilles]
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]
Hibernate can also generate a UUID based on IETF RFC 4122 version 1. Following the specification, you should generate the UUID with the MAC address instead of the IP address. As long as nobody is messing around with it, the MAC address of each device should be unique, and due to this help to create a unique UUID.
Hibernate uses the IP address instead of the MAC address. In general, this is not an issue. But if the servers of your distributed system are running on different networks, you should make sure that none of them share the same IP address.
The configuration of the IETF RFC 4122 version 1 based UUID generator is very similar to the previous one.
The @UuidGenerator annotation introduced in Hibernate 6 has a style attribute that you can use to define how Hibernate shall generate the UUID value. When you set it to TIME, it uses a timestamp and the IP address to generate the UUID value.
@Entity
public class Book {
@Id
@GeneratedValue
@UuidGenerator(style = Style.TIME)
private UUID id;
...
}
As you can see in the code snippet, the only difference from the previous section is the value of the strategy attribute. Everything else is still the same.
Let’s use this mapping to persist a new Book entity object.
Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);
As you can see, the log output looks similar to the previous test executions. Hibernate generates a new UUID value and uses it to set the id attribute, before it persists a new record in the Book table.
18:28:57,068 DEBUG AbstractSaveEventListener:127 - Generated identifier: c0a8b235-8207-1771-8182-07d7756a0000, using strategy: org.hibernate.id.uuid.UuidGenerator
18:28:57,095 DEBUG SQL:128 - insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
18:28:57,101 TRACE bind:28 - binding parameter [1] as [DATE] - [1902-04-30]
18:28:57,101 TRACE bind:28 - binding parameter [2] as [VARCHAR] - [The Hound of the Baskervilles]
18:28:57,102 TRACE bind:28 - binding parameter [3] as [INTEGER] - [0]
18:28:57,102 TRACE bind:28 - binding parameter [4] as [BINARY] - [c0a8b235-8207-1771-8182-07d7756a0000]
If you’re using Hibernate 4 or 5, you need to set an additional parameter on the @GenericGenerator annotation to define the generation strategy. You can see an example of it in the following code snippet.
You define the strategy by providing a @Parameter annotation with the name uuid_gen_strategy_class and the fully qualified class name of the generation strategy as the value.
@Entity
public class Book {
@Id
@GeneratedValue(generator = "UUID")
@GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator",
parameters = {
@Parameter(
name = "uuid_gen_strategy_class",
value = "org.hibernate.id.uuid.CustomVersionOneStrategy"
)
}
)
@Column(name = "id", updatable = false, nullable = false)
private UUID id;
…
}
When you now persist the new Book entity, Hibernate will use the CustomVersionOneStrategy class to generate the UUID based on IETF RFC 4122 version 1.
Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);
As you can see in the log output, Hibernate uses both strategies in the same way.
12:35:22,760 DEBUG AbstractSaveEventListener:118 – Generated identifier: c0a8b214-578f-131a-8157-8f431d060000, using strategy: org.hibernate.id.UUIDGenerator
12:35:22,792 DEBUG SQL:92 – insert into Book (publishingDate, title, version, id) values (?, ?, ?, ?)
12:35:22,795 TRACE BasicBinder:65 – binding parameter [1] as [DATE] – [1902-04-30]
12:35:22,795 TRACE BasicBinder:65 – binding parameter [2] as [VARCHAR] – [The Hound of the Baskervilles]
12:35:22,796 TRACE BasicBinder:65 – binding parameter [3] as [INTEGER] – [0]
12:35:22,797 TRACE BasicBinder:65 – binding parameter [4] as [OTHER] – [c0a8b214-578f-131a-8157-8f431d060000]
As you’ve seen, you can use UUIDs as primary keys, and JPA and Hibernate define different ways to generate UUID values.
JPA 3.1 adds the value UUID to the GenerationType enum and requires the persistence provider to generate a UUID based on IETF RFC 4122. But it doesn’t define which of the 4 approaches shall be used and doesn’t provide any portable way to customize the UUID generation.
Hibernate can generate UUID values for several years now. In versions 4 and 5, you need to use a @GenericGenerator and provide the class of the generator you want to use. Hibernate 6 simplified this by introducing the @UuidGenerator annotation for it.