从零开始 Spring Boot 64:Hibernate 标识符
图源:简书 (jianshu.com)
Hibernate 中的实体,由标识符(Identitifier)确定了其实体实例的唯一性,这对应于表中的主键。
@Id
对于单一属性作为标识符的情况,可以用@Id
注解标注:
@Entity(name = "Person5")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Person {
@Id
private Long id;
}
被@Id
标注的属性类型必须是 Java 基本类型(Primitive)或基本类型的包装器类型(Wrapper Type),除此之外,还包括 String
、Date
、BigDecimal
和BigInteger
。
生成标识符
用 JPA 向数据库添加持久数据的时候,需要为实体指定一个标识符。通常并不需要我们手动完成,利用 JPA 的标识符生成器(Identitifier Generator)就可以自动生成一个标识符。
在实体类中,可以用@GeneratedValue
指示通过标识符生成器来生成标识符:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
@Column(unique = true)
private String name;
}
@GeneratedValue
的strategy
属性指定了用何种方式生成标识符:
TABLE
,用基础表生成SEQUENCE
,用序列生成IDENTITY
,用表的自增主键生成UUID
,成成UUIDAUTO
,默认值,根据属性类别自动进行处理
AUTO
当生成策略(Strategy)是AUTO
时,如果作为标识符的属性类型是数字(比如Long
),JPA 会使用序列生成标识符。具体来说就是生成一个xxx_seq
名称的表,该表只保存一行一个字段的值,该值记录用于生成标识符的序列当前值。
比如在这个示例中:
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
// ...
}
DDL:
CREATE TABLE `student` (
`id` bigint NOT NULL,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_7pb8owoegbhhcrpopw4o1ykcr` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `student_seq` (
`next_val` bigint DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
如果标识符属性类型是 UUID
,则使用 UUIDGenerator 生成标识符:
@Entity(name = "Student2")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
// ...
}
这里的 UUID 是
java.util.UUID
,而非 JPA 的UUID
类型。
DDL:
CREATE TABLE `student2` (
`id` binary(16) NOT NULL,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_sj36kwoqdheqatod12i8s5qkf` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
注意,这里用于保存 UUID 标识符的id
字段类型为binary(16)
,也就是16字节(byte)。UUID 是32位16进制数组成的,一个字节可以表示8位二进制数,转换一下就可以表示2位16进制数,16个字节正好可以表示32位16进制数。
因为是二进制形式保存,所以用数据库可视化工具(比如 Sqlyog)查看会是乱码。
在控制台打印持久化后的实体示例可以看到类似这样的内容:
Student(id=65087c03-23e3-4d65-9dc4-6be2be8cad48, name=BrusLee)
Student(id=40084952-3742-46d5-a903-245237ed9168, name=icexmoon)
Student(id=817073e3-0c86-453d-8454-e00a0316bf38, name=JackChen)
id 属性中的内容就是生成的UUID。
IDENTITY
使用这种策略,会使用数据库表的自增主键作为标识符:
@Entity(name = "Student3")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ...
}
DDL:
CREATE TABLE `student3` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_m4nlv947n7v1mi89yss97orr3` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
TABLE
与 SEQUENCE 类似,不同的是不会单独建表来保存某个表的序列值,而是用一个统一的表。
比如:
@Entity(name = "Student4")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
// ...
}
DDL:
CREATE TABLE `student4` (
`id` bigint NOT NULL,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_265w6kd3gs9mgm2ua0cf8gvoe` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
CREATE TABLE `hibernate_sequences` (
`sequence_name` varchar(255) NOT NULL,
`next_val` bigint DEFAULT NULL,
PRIMARY KEY (`sequence_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
默认情况下不同的实体使用相同的表(默认为hibernate_sequences
)中同一个序列值(sequence_name='default'
)生成标识符,不过这个设置可以手动修改:
@Entity(name = "Student6")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "student-id-generator")
@TableGenerator(name = "student6-id-generator",
table = "hibernate_sequences",
pkColumnName = "sequence_name",
pkColumnValue = "student6-id",
valueColumnName = "next_val",
allocationSize = 1
)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
@Column(unique = true)
private String name;
}
@TableGenerator
包含以下属性:
name
,生成器名称,可以在@GeneratedValue
的generator
属性中关联。table
,保存生成器序列的表名。pkColumnName
,用于区分不同序列的列名。pkColumnValue
,当前生成器用于区分序列的列值。valueColumnName
,保存序列值的列名。allocationSize
,每次批量分配的序列跨度,默认为50。
使用这种方式分配标识符的好处是可以批量持久化数据,所以allocationSize
默认为50。负面效果是序列消耗很快,如果单次运行程序仅添加了1个实体实例,序列同样会+50。如果要改变这一点,可以设置allocationSize
为1。
UUID
在之前介绍 AUTO 策略的时候已经演示过 UUID 类型和 UUID 生成器的用途了。实际上UUID就是一串32位16进制数,所以也可以用字符串类型的标识符保存 UUID:
@Entity(name = "Student5")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.UUID)
@Length(max = 36)
private String id;
// ...
}
字符串形式的 UUID 除了32位的16进制数,还有4位-
构成的分隔符,比如:
2eb1ba80-1b8e-4db7-8a82-6a5721f2e749
因此这里将标识符id
的长度设置为36。
JPA 生成的表结构:
CREATE TABLE `student5` (
`id` varchar(36) NOT NULL,
`name` varchar(45) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `UK_juaktqxrqswivtnlhdo9mspmc` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
当然,这样做会浪费一些存储空间,但好处是可以用可视化工具查看表的UUID主键。
SEQUENCE
SEQUENCE 生成策略已经在 AUTO 中介绍过了,与 TABLE 类似,同样可以修改默认的 JPA 行为:
@Entity(name = "Student7")
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "student7-id-generator")
@SequenceGenerator(name = "student7-id-generator",
sequenceName = "student7_seq",
initialValue = 5,
allocationSize = 1)
private Long id;
// ...
}
现在数据库中会创建一个表student7_seq
来保存序列,且初始的值是5而非1,每次分配的跨度也是1而非50。
自定义标识符生成器
可以自定义标识符生成器。自定义标识符生成器需要实现IdentifierGenerator
接口,如果需要从外部添加参数,还需要实现Configurable
接口:
public class MyGenerator implements IdentifierGenerator, Configurable {
private String prefix;
@Override
public void configure(Type type, Properties parameters, ServiceRegistry serviceRegistry) {
IdentifierGenerator.super.configure(type, parameters, serviceRegistry);
prefix = parameters.getProperty("prefix");
}
@Override
public Object generate(SharedSessionContractImplementor session, Object obj) {
EntityPersister entityPersister = session.getEntityPersister(obj.getClass().getName(), obj);
String query = String.format("select %s from %s",
entityPersister.getIdentifierPropertyName(),
entityPersister.getEntityName());
Stream<String> ids = session.createQuery(query, String.class).stream();
Long max = ids.map(o -> o.replace(prefix + "-", ""))
.mapToLong(Long::parseLong)
.max()
.orElse(0L);
return prefix + "-" + (max + 1);
}
}
在上面这个示例中,标识符生成器MyGenerator
可以接收一个参数作为生成标识符的前缀。生成标识符的规则为,先查询获取实体的主键,并去除前缀后选取最大值,+1后作为分配的下一个 key,然后加上前缀。
这个示例修改自这篇文章。
使用自定义标识符生成器:
@Entity(name = "Student8")
public class Student {
@Id
@GeneratedValue(generator = "student8-id-generator")
@GenericGenerator(name = "student8-id-generator",
parameters = @Parameter(name = "prefix", value = "student8-id"),
type = MyGenerator.class)
private String id;
// ...
}
测试用例中生成的标识符类似下面这样:
Student(id=student8-id-2, name=BrusLee)
Student(id=student8-id-1, name=icexmoon)
Student(id=student8-id-3, name=JackChen)
复合标识符
多个属性共同作为标识符的方式称为“复合标识符”(Composite Identifiers),这对应表结构中的联合主键。
可以用@Embeddable
+@Embedded
或者@IdClass
定义符合标识符。相关内容可以阅读这篇文章中的联合主键部分。
派生标识符
派生标识符(Derived Identifier)指的是来自别的实体类的标识符,这对应表结构中作为外键的主键。
可以用@MapsId
定义派生标识符的来源,比如:
@Entity
public class UserProfile {
@Id
private long profileId;
@OneToOne
@MapsId
private User user;
// ...
}
更多关于派生标识符的使用示例可以阅读:
- 从零开始 Spring Boot 56:JPA中的一对一关系 - 红茶的个人站点 (icexmoon.cn)
- 从零开始 Spring Boot 57:JPA中的一对多关系 - 红茶的个人站点 (icexmoon.cn)
- 从零开始 Spring Boot 58:JPA中的多对多关系 - 红茶的个人站点 (icexmoon.cn)
The End,谢谢阅读。
可以从这里获取本文的完整示例代码。
参考资料
- UUID是如何保证唯一性的? - 知乎 (zhihu.com)
- Hibernate Inheritance Mapping | Baeldung