SpringDataJPA主键生成策略大全
1. 前言
我们在定义实体类的时候,会使用到一个@Id 的注解,该注解标识这个字段是唯一的,是数据库表中的主键。持久化的时候不能为null,也不会被修改。事实上Hibernate给我们提供了几种不同的生成主键方式来定义主键列。
本文将逐一讲解每种方式。
2. 基本主键标识
定义主键标识的最直接方法是使用@Id注解
使用@Id注解可以映射到以下数据类型之一的单个属性:Java基本类型和基本包装类型,String,Date,BigDecimal,BigInteger。
我们来看一个简单的例子,该例子定义一个主键类型为Integer的实体:
为了更简单明了,我会习惯地在下面的示例代码中忽略不相干的包引用、属性、getter setter 构造函数等
/**
* 用户实体类
*
* @author xtoad
* @date 2020/05/29
*/
@Entity
@Table(appliesTo = "user", comment = "用户表")
@EntityListeners(AuditingEntityListener.class)
public class User {
private static final long serialVersionUID = -1616905035103332302L;
/**
* ID
*/
@Id
private Integer id;
}
上面的代码只是会标记id为主键,但是主键值的生成方式我们需要使用另外一个注解指定。
3. 自动生成主键值
如果要自动生成主键值,可以使用@GeneratedValue注解。并且可以通过设定strategy 属性值,来指定具体的键值生成方式。
一共有四种方式:GenerationType.AUTO,GenerationType.IDENTITY,GenerationType.SEQUENCE,GenerationType.TABLE
默认是GenerationType.AUTO。
3.1. GenerationType.AUTO 方式
如果使用默认的AUTO方式生成主键值,那么数据库持久层程序会根据主键属性的值类型来确定要如何生成主键值。
此时主键字段的数据类型可以是数值或者UUID。
对于数值类型的,会生成基于序列或表生成器,而UUID类型的会使用UUIDGenerator。
我们看一下AUTO生成策略映射的实体主键的示例:
@Entity
@Table(appliesTo = "user", comment = "用户表")
@EntityListeners(AuditingEntityListener.class)
public class User {
private static final long serialVersionUID = -1616905035103332302L;
/**
* ID
*/
@Id
@GeneratedValue
private Integer id;
}
此时,id字段将在数据库级别是唯一的。
Hibernate 5中引入的一个有趣的功能是UUIDGenerator。要使用此功能,我们需要做的就是声明一个带有@GeneratedValue批注的UUID类型的ID :
@Entity
@Table(appliesTo = "user", comment = "用户表")
@EntityListeners(AuditingEntityListener.class)
public class User {
private static final long serialVersionUID = -1616905035103332302L;
/**
* ID
*/
@Id
@GeneratedValue
private UUID id;
}
Hibernate将生成类似为“b72b5743-6d21-4542-90e7-7c4bdfbbdeef”的ID。
这种方式生成的id是全球唯一的,通常这种方式用于分布式系统解决方案能达到很好的效果,不需要考虑id的连续性和重复性。
3.2. GenerationType.IDENTITY方式
这种类型的生成依赖于IdentityGenerator,现实方式是依赖于数据库中的Identity列,插入数据时该列的值是自动递增的。
要使用这种生成类型,我们只需要设置策略参数如下:
@Entity
@Table(appliesTo = "user", comment = "用户表")
@EntityListeners(AuditingEntityListener.class)
public class User {
private static final long serialVersionUID = -1616905035103332302L;
/**
* ID
*/
@Id
@GeneratedValue (strategy = GenerationType.IDENTITY)
private UUID id;
}
需要注意的是:Hibernates使用IDENTITY生成器禁用对实体的JDBC批处理支持。
3.3. GenerationType.SEQUENCE方式
Hibernate提供了SequenceStyleGenerator类来支持基于序列的id。
如果采用了该策略,会判断我们使用的数据库是否支持序列,如果支持会使用数据的序列生成;如果不支持,则使用序列生成器。
我们还可以自定义序列名称和一些属性,将@GenericGenerator 注解与SequenceStyleGenerator策略一起使用:
@Entity
@Table(appliesTo = "user", comment = "用户表")
@EntityListeners(AuditingEntityListener.class)
public class User {
private static final long serialVersionUID = -1616905035103332302L;
/**
* ID
*/
@Id
@GeneratedValue(generator = "sequence-generator")
@GenericGenerator(
name = "sequence-generator",
strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
parameters = {
@Parameter(name = "sequence_name", value = "user_sequence"),
@Parameter(name = "initial_value", value = "4"),
@Parameter(name = "increment_size", value = "1")
}
)
private Integer id;
}
上面的示例中,我们还为序列设置了初始值,这意味着主键生成将从4开始。
SEQUENCE是Hibernate文档推荐的生成类型。
每个序列生成的值都是唯一的。如果未指定序列名称,则Hibernate将对不同类型重复使用相同的hibernate_sequence。
3.4. GenerationType.TABLE方式
这种策略,会生成以上表,专门来存储主键值。这样可以不依赖于特定的数据库服务器,可以方便地进行不同环境的移植。缺点是会损失部分性能。该策略一般与另外一个注解一起使用@TableGenerator。
@Entity
@Table(appliesTo = "user", comment = "用户表")
@EntityListeners(AuditingEntityListener.class)
public class User {
private static final long serialVersionUID = -1616905035103332302L;
/**
* ID
*/
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "table-generator")
@TableGenerator(name = "table-generator",
table = "user_ids",
pkColumnName = "seq_id",
valueColumnName = "seq_value",
initialValue = 1,
allocationSize=1 )
private Integer id;
}
上面的例子中每个属性都很好理解,会生成如下一张表和数据:
我们每对user表插入一条记录,user_ids 表中 seq_id = 'user' 行的seq_value值就会增加1。
以上每种方式都可以生成唯一的主键值,只是采取不同策略时Hibernate实现的方式些差别。
3.5. 自定义主键生成策略
如果我们不想使用任何现成的策略,则可以通过实现*IdentifierGenerator*接口来定义自定义生成器。
下面我们来创建一个主键生成器,该生成器生成包含字符前缀和数字的标识符:
public class MyGenerator
implements IdentifierGenerator, Configurable {
private String prefix;
@Override
public Serializable generate(
SharedSessionContractImplementor session, Object obj)
throws HibernateException {
String query = String.format("select %s from %s",
session.getEntityPersister(obj.getClass().getName(), obj)
.getIdentifierPropertyName(),
obj.getClass().getSimpleName());
Stream ids = session.createQuery(query).stream();
Integer max = ids.map(o -> o.replace(prefix + "-", ""))
.mapToInt(Integer::parseInt)
.max()
.orElse(0);
return prefix + "-" + (max + 1);
}
@Override
public void configure(Type type, Properties properties,
ServiceRegistry serviceRegistry) throws MappingException {
prefix = properties.getProperty("prefix");
}
}
在此示例中,我们实现了IdentifierGenerator接口并重写了generate()方法。
并首,从前缀XX形式的现有主键中找到了最大的数字。
然后,将最大数加一,并附加prefix属性以获得新生成的id值。
我们的类还实现了Configurable接口,因此我们可以在configure()方法中设置prefix属性值。
接下来,我们就可以将自定义生成器添加到实体中。使用@GenericGenerator注解,并指定strategy参数。该参数需包含生成器类的完整类名:
@Entity
@Table(appliesTo = "user", comment = "用户表")
@EntityListeners(AuditingEntityListener.class)
public class User {
private static final long serialVersionUID = -1616905035103332302L;
/**
* ID
*/
@Id
@GeneratedValue(generator = "user-generator")
@GenericGenerator(name = "user-generator",
parameters = @Parameter(name = "prefix", value = "user"),
strategy = "com.xtoad.ecms.common.web.generator.MyGenerator")
private String id;
}
上面例子中我们将prefix参数设置为“user”
然后我们每插入user表中的数据id 值会是这样子的:user-1、user-2……
4. 复合主键
上面的实例中都是单一主键,实际上我们设计数据库时,是可以使用复合主键的,即多个字段作为主键。
Hibernate对此也做了相应的支持。
复合主键由具有一个或多个持久属性的主键类表示。
主键类必须满足以下条件:
- 它应该使用@EmbeddedId或@IdClass注解定义
- 它应该是公共的,可序列化的并且具有公共的无参数构造函数
- 它应该实现equals()和hashCode()*方法
该类的属性可以是基本属性,复合属性或ManyToOne,同时避免使用collections和OneToOne属性。
4.1. 使用@EmbeddedId注解定义复合主键
首先,我们需要使用@Embeddable定义一个复合主键类,直接看示例:
/**
* 学生课程组合键类
*
* @author xtoad
* @date 2021/3/7
*/
@Embeddable
public class CourseRatingKey implements Serializable {
/**
* 学生ID
*/
@Column(name = "student_id")
private Integer studentId;
/**
* 课程ID
*/
@Column(name = "course_id")
private Integer courseId;
///////////////// 忽略了getter 和 setter
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CourseRatingKey that = (CourseRatingKey) o;
return Objects.equals(studentId, that.studentId) &&
Objects.equals(courseId, that.courseId);
}
@Override
public int hashCode() {
return Objects.hash(studentId, courseId);
}
}
接下来,我们可以使用@ EmbeddedId将CourseRatingKey类型的ID添加到实体中:
/**
* 课程评分实体
*
* @author xtoad
* @date 2021/3/7
*/
@Entity
public class CourseRating {
/**
* 组合键
*/
@EmbeddedId
private CourseRatingKey id;
}
上面的示例会生成如下的表结构:
course_rating表的主键是由course_id 和 student_id组成的复合主键。
上面这种方式,我们在 SpringDataJpa实体关联映射ManyToMany 的文章中都过介绍。
4.2. 使用@IdClass注解定义复合键
@IdClass注解类似,也需要先建立一个主键类。主键的定义也和前面的相同。
所不同的是在使用复合主键的实体类中的写法不同。
还是上面的例子,我们使用@IdClass 方式实现复合主键如下:
/**
* 课程评分实体
*
* @author xtoad
* @date 2021/3/7
*/
@Entity
@IdClass(CourseRatingKey.class)
public class CourseRating {
/**
* 学生ID
*/
@Id
private Integer studentId;
/**
* 课程ID
*/
@Id
private Integer courseId;
}
此时我们使用的时候可以直接在CourseRating实体类的对象上设置studentId 和courseId, 而不需要先创建主键类。
5. 共享主键@MapsId
使用@MapsId注解可以从实体的关联中共享主键。
这种方式我们在 SpringDataJPA中OneToOne的三种方式 一文中有过介绍。
直接看示例:
/**
* 用户扩展信息实体类
*
* @author xtoad
* @date 2020/05/29
*/
@Entity
public class UserExtra {
@Id
private Integer id;
@OneToOne
@MapsId
private User user;
// ...
}
/**
* 用户实体类
*
* @author xtoad
* @date 2020/05/29
*/
@Entity
public class User {
@Id
private Integer id;
/**
* 用户扩展信息
*/
@OneToOne(mappedBy = "user")
@PrimaryKeyJoinColumn
private UserExtra userExtra;
// ...
}
此时,UserExtra表中的主键id是直接关联的user表中的主键id,插入的时候值也会使用user表的id值。
6. 小结
本文中,我们学习了SpringDataJPA中如何设置主键策略,以及自定义主键策略,还有如何设置复合主键等。文中分别对每种不同实现做了详细的介绍和代码示例。