如何用Hibernate生成UUID作为主键(附代码示例)

930 阅读10分钟

大多数开发者更喜欢数字主键,因为它们使用效率高,容易生成。但这并不意味着主键必须是一个数字。

例如,UUIDs在最近几年得到了一些欢迎。UUID的主要优点是它的(实际)全球唯一性,为分布式系统提供了巨大的优势。

如果你使用典型的数字ID,每条新记录都会被递增,你需要由同一个系统组件生成所有的ID。在大多数情况下,这是由你的数据库管理的每个表的一个序列。这使得该序列成为失败的单一来源。其他方法,例如,集群数据库或任何其他横向扩展的数字发生器,需要在节点之间进行通信。这显然会产生一些努力,使你的主键值的生成速度减慢。

当使用全局唯一的UUID时,你就不需要这些了。每个组件都可以生成自己的UUID,而且不会有任何冲突。这就是为什么UUID在基于微服务的架构中或在开发离线客户端时变得流行。

另一方面,UUID也有一些缺点。最明显的一个是它的大小。它比数字ID大4倍,而且不能被有效地处理。因此,你应该仔细决定是否要使用UUID或数字ID,并与你的数据库管理员讨论。

如果你决定使用UUIDs,你当然也可以用Hibernate持久化它们。当这样做时,你需要决定如何生成UUID值。当然,你可以自己生成它,并在持久化之前将其设置在实体对象上。或者,如果你使用的是Hibernate 4、5、6或者JPA 3.1,你可以在实体映射中定义一个生成策略。我将在本文中告诉你如何做到这一点。

使用JPA 3.1生成UUIDs

从JPA 3.1开始,你可以用*@GeneratedValue来注释一个主键属性,并将策略设置为GenerationType.UUID。* 基于该规范,你的持久化提供者应根据IETF RFC 4122生成一个UUID值:

@Entity
public class Book {

	@Id
	@GeneratedValue(strategy = GenerationType.UUID)
	private UUID id;
	
	…
}

让我们试试这个映射并持久化一个新的Book 实体对象:

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

你可以在日志输出中看到,Hibernate生成了一个UUID 值,并在将其持久化到数据库中之前将其设置在Book 实体对象上:

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定义了4种不同的策略来生成UUIDs。但不幸的是,JPA 3.1并没有指定你的持久化提供者应使用哪个版本。它也没有定义任何可移植的机制来定制这个生成过程。

因此,你的持久化提供者可以决定它如何生成UUID值。而且这种行为在JPA实现之间可能会有所不同。

当你使用Hibernate作为你的持久化提供者时,它会根据IETF RFC 4122版本的定义,基于随机数生成UUID值。当我向你展示Hibernate专有的UUID生成器时,我将深入了解这方面的细节。

使用Hibernate 4、5和6生成UUID

如前所述,IETF RFC 4122定义了4种不同的策略来生成UUIDs。Hibernate支持其中的2种:

  1. 默认策略是基于随机数生成UUID(IETF RFC 4122版本4)。
  2. 你也可以配置一个使用机器的IP地址和时间戳的生成器(IETF RFC 4122 Version 1)。

你要使用的策略的定义取决于你的Hibernate版本。我们先来看看默认策略。

基于随机数的UUID(IETF RFC 4122版本4)

默认情况下,Hibernate使用基于随机数的生成策略。如果你使用之前描述的、基于JPA的定义,这也是Hibernate使用的策略。

Hibernate 6中基于随机数的UUID

使用Hibernate 6,你可以用*@UuidGenerator来注释你的主键属性,并将样式设置为RANDOM*、AUTO或不指定它。在这三种情况下,Hibernate将应用其默认策略:

@Entity
public class Book {
	
	@Id
	@GeneratedValue
    @UuidGenerator
	private UUID id;

	...
}

让我们把这个映射与测试一起使用,就像我之前给你看的那样:

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

不出所料,这将得到与之前测试案例中相同的日志输出。在内部,当我使用JPA的映射注解时,Hibernate也使用了同样的风格:

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]

Hibernate 4和5中基于随机数的UUID

如果你使用Hibernate 4或5,你可以使用同样的功能。但是你需要在你的映射定义中投入一点额外的努力。

你需要用*@GeneratedValue注解来注释你的主键属性。在该注解中,你需要引用一个自定义生成器,并使用Hibernate的@GenericGenerator注解定义该生成器。@GenericGenerator注解需要2个参数,生成器的名称和实现生成器的类的名称。在这种情况下,我把生成器称为 "UUID",Hibernate将使用org.hibernate.id.UUIDGenerator*类。

@Entity
public class Book {

	@Id
	@GeneratedValue(generator = "UUID")
	@GenericGenerator(
		name = "UUID",
		strategy = "org.hibernate.id.UUIDGenerator",
	)
	private UUID id;
	
	…
}

这就是你需要做的,告诉Hibernate生成一个UUID作为主键。让我们使用这个映射来持久化一个新的Book实体对象:

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

正如你在日志输出中看到的,Hibernate生成了一个UUID,并在向数据库写入新记录之前将其设置为id值:

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]

基于IP和时间戳的UUID(IETF RFC 4122版本1)

Hibernate也可以基于IETF RFC 4122版本1生成UUID。按照该规范,你应该用MAC地址而不是IP地址来生成UUID。只要没有人捣乱,每个设备的MAC地址应该是唯一的,由于这个原因,有助于创建一个唯一的UUID。

Hibernate使用IP地址而不是MAC地址。一般来说,这不是一个问题。但如果你的分布式系统的服务器运行在不同的网络上,你应该确保它们都不共享相同的IP地址。

基于IETF RFC 4122版本1的UUID生成器的配置与之前的配置非常相似。

Hibernate 6中基于IP和时间戳的UUID

Hibernate 6中引入的*@UuidGenerator* 注解有一个样式 属性,你可以用它来定义Hibernate应如何生成UUID值。当你把它设置为TIME时,它使用时间戳和IP地址来生成UUID值:

@Entity
public class Book {
	
	@Id
	@GeneratedValue
    @UuidGenerator(style = Style.TIME)
	private UUID id;

	...
}

正如你在代码片段中所看到的,与上一节的唯一区别是策略属性的值。其他的都是一样的。

让我们使用这个映射来持久化一个新的Book 实体对象:

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

正如你所看到的,日志输出看起来与之前的测试执行类似。Hibernate生成了一个新的UUID值,并使用它来设置id属性,然后再持久化Book 表中的一个新记录:

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]

Hibernate 4和5中基于IP和时间戳的UUID

如果你使用Hibernate 4或5,你需要在*@GenericGenerator*注解上设置一个额外的参数来定义生成策略。你可以在下面的代码片断中看到一个例子。

你通过提供一个名为uuid_gen_strategy_class的*@Parameter*注解来定义策略,并将生成策略的完全合格的类名作为值:

@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;
	
	…
}

当你现在坚持新的Book实体时,Hibernate将使用CustomVersionOneStrategy类来生成基于IETF RFC 4122版本1的UUID:

Book b = new Book();
b.setTitle("The Hound of the Baskervilles");
b.setPublishingDate(LocalDate.of(1902, 4, 30));
em.persist(b);

正如你在日志输出中看到的,Hibernate以同样的方式使用这两种策略:

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]

总结

正如你所看到的,你可以使用UUIDs作为主键,JPA和Hibernate定义了不同的方式来生成UUID值。

JPA 3.1在GenerationType 枚举中加入了UUID值,并要求持久化提供者根据IETF RFC 4122生成UUID。但它没有定义这4种方法中的哪一种,也没有提供任何可移植的方法来定制UUID的生成。

Hibernate能够生成UUID值已经有好几年了。在版本4和5中,你需要使用*@GenericGenerator并提供你想使用的生成器的类。Hibernate 6通过为其引入@UuidGenerator*注解简化了这一点。