Hibernate JPA 主键策略
Hibernate JPA 生成主键主要通过:@Id 和 @GeneratedValue 注解实现,其生成规则由 @GeneratedValue 设定
@GeneratedValue的源码:
@Target({METHOD,FIELD})
@Retention(RUNTIME)
public @interface GeneratedValue{
GenerationType strategy() default AUTO;
String generator() default "";
}
其中GenerationType枚举属性:
public enum GenerationType{
TABLE,
SEQUENCE,
IDENTITY,
AUTO
}
JPA 4种主键策略分别为:AUTO、IDENTITY、SEQUENCE、TABLE。剩下都是Hibernate自己的策略,包括我们常用的 native、uuid7n、assigned、sequence
-
HAUTO:JPA自动选择合适的策略,是默认选项
-
IDENTITY:采用数据库 ID自增长的方式来自增主键字段,Oracle 不支持这种方式
-
SEQUENCE:通过序列产生主键,通过
@SequenceGenerator
注解指定序列名,MySql 不支持这种方式 -
TABLE:通过一张数据库表的形式帮助我们完成主键自增
1、AUTO 策略
默认的配置。如果不指定主键生成策略,默认 AUTO。设置自动主键策略,在保存对象时可以自己设置主键值,也可以不填。示例(任选其一都可以)
// 方式一:如果AUTO可以不用配置@GeneratedValue,默认就是AUTO设置
@Id
private Long Id;
// 方式二:默认配置@GeneratedValue,strategy属性default AUTO
@Id
@GeneratedValue
private Long Id;
// 方式三:完整配置
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long Id;
PS 注意(在使用AUTO策略时):
- 如果是MySQL数据库,一定要将数据库的主键列设置成自增长,否则使用AUTO策略的时候,会报错:
org.hibernate.exception.GenericJDBCException: Field 'id' doesn't have a default value
- 如果是Oracle数据库,那么会使用
hibernate_sequence
,这个名称是固定的,不能更改。
2、IDENTITY 策略
主键则由数据库自动维护,底层数据库必须支持自动增长(对id自增,MySQL支持,Oracle不支持)示例:
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
PS:设置了主键自增的话建议在保存对象是就不要设置主键Id值了,会报错。如果想手动设置值可以先注释@GeneratedValue
3、SEQUENCE 策略
@SequenceGenerator 注解的使。该策略一般会 @GeneratedValue 与 @SequenceGenerator 注解同时使用
GenerationType.SEQUENCE:在某些数据库中不支持主键自增长。如Oracle
,其提供了一种叫做**序列(sequence)**的机制生成主键。该策略不足之处正好与TABLE
相反,由于只有部分数据库(Oracle、PostgreSQL、DB2)支持序列对象。所以该策略一般不应用其他数据库。该策略一般与 @SequenceGenerator 注解一起使用,该注解指定了生成主键的序列,然后 JPA 会根据注解内容创建一个序列(或使用一个现有序列)如果不指定序列,则使用厂商提供的默认序列生成器:Hibernate
默认提供序列名称为HIBERNATE_SEQUENCE
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "id_sequence")
@SequenceGenerator(name="id_sequence", initialValue=8, allocationSize=1, sequenceName="ID_SEQUENCE")
private int id;
@SequenceGenerator 注解的定义(用来指定序列的相关信息)
@Repeatable(SequenceGenerators.class)
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface SequenceGenerator {
String name(); // 序列生成器的名称,会在@GeneratedValue中进行引用
String sequenceName() default ""; // 表示生成策略用到的数据库序列名称
String catalog() default ""; // 指定生成序列号的表的 schema
String schema() default ""; // 指定生成序列号的表的 schema
int initialValue() default 1; // 主键的初始值,默认为0
int allocationSize() default 50; // 主键每次增长值的大小,默认为50
}
属性 | 说明 |
---|---|
name | 该属性是必须设置的属性,序列生成器的名称,会在@GeneratedValue 中进行引用 |
sequenceName | 实体标识所使用的数据库序列号的名称。该属性是可选的,如果我们没有为该属性设置值,OpenJPA 框架将自动创建名为 OPENJPA_SEQUENCE的序列号。如果一个 OpenJPA 容器中管理的多个实体都选择使用序列号机制生成实体标识,而且实体类中都没有指定标识字段的sequenceName属性,那么这些实体将会共享系统提供的默认名为 OPENJPA_SEQUENCE的序列号。这可能引起实体类编号的不连续。我们可以用下面的这个简单例子说明这种情况:假设 OpenJPA 容器中存在两个实体类 Dog 和 Fish,它们的实体标识字段都是数值型,并且都选择使用序列号生成实体标识,但是实体类中并没有提供sequenceName属性值。当我们首先持久化一个 Dog 对象时,它的实体标识将会是 1,紧接着我们持久化一个 Fish 对象,它的实体标识就是 2,依次类推。 |
initialValue | 该属性设置所使用序列号的起始值。默认为0 |
allocationSize | 一些数据库的序列化机制允许预先分配序列号,比如 Oracle,这种预先分配机制可以一次性生成多个序列号,然后放在 cache 中,数据库用户获取的序列号是从序列号 cache 中获取的,这样就避免了在每一次数据库用户获取序列号的时候都要重新生成序列号。allocationSize属性设置的就是一次预先分配序列号的数目,默认情况下allocationSize属性的值是 50。 |
schema | 该属性设置的是生成序列号的表的 schema。该属性并不是必须设置的属性,如果开发者没有为该属性设置值,OpenJPA 容器将会默认使用当前数据库用户对应的 schema。 |
catalog | 该属性设置的是生成序列号的表的 catalog。该属性并不是必须设置的属性,如果开发者没有为该属性设置值,OpenJPA 容器将会使用默认当前数据库用户对应的 catalog。 |
PS:如果底层数据库不执行序列,会报错:org.hibernate.MappingException: org.hibernate.dialect.MySQLDialect does not support sequences
SQL创建序列:
CREATE SEQUENCE seqTest INCREMENT BY 1 -- 每次加几个 START WITH 1 -- 从1开始计数 NOMAXvalue -- 不设置最大值 NOCYCLE -- 一直累加,不循环 CACHE 10; -- 设置缓存cache个序列,如果系统down掉了或者其它情况将会导致序列不连续,也可以设置为:NOCACHE create sequence s_config_para maxvalue 4294967295 -- 设置最大值为4294967295 cycle; -- 设置cycle属性,当达到最大值时,不是从start with设置的值开始循环。而是从1开始循环
SQL创建序列链接:https://blog.csdn.net/jiejie5945/article/details/44198283
4、TABLE 策略
主要使用 @TableGenerator 注解,GenerationType.TABLE :使用一张特殊的数据库表,保存插入记录的时,需要的主键值。
有时候为了不依赖于数据库的具体实现,在不同数据库之间更好的移植,可以在数据库中新建序列表来生成主键,序列表一般包含两个字段:第一个字段引用不同的关系表(表名),第二个字段是该关系表的最大序号。这样,只需要一张序列就可以用于多张表的主键生成。
@TableGenerator 注解的定义
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface TableGenerator {
String name();
String table() default "";
String catalog() default "";
String schema() default "";
String pkColumnName() default "";
String valueColumnName() default "";
String pkColumnValue() default "";
int initialValue() default 0;
int allocationSize() default 50;
UniqueConstraint[] uniqueConstraints() default {};
}
其中属性说明:
属性名 | 解释 |
---|---|
name | 对应GeneratedValue的generator属性值。通过俩者将其互相关联 |
table | 对应第三方主键生成表名[代表JPA将使用哪个第三方表来主键值得计算] |
schema | 指定生成序列号的表的 schema。如没设置,OpenJPA 容器将会默认使用当前数据库用户对应的 schema |
catalog | 指定生成序列号的表的 catalog。如没设值,OpenJPA 容器将会使用默认当前数据库用户对应的 catalog |
pkColumnName | 指定第三方表中对应的某个列名 |
valueColumnName | 指定生成的列名[对应第三方表的另外一个列值 |
pkColumnValue | 指定第三方表中对应的某个列的值,某个列指 pkColumnName属性中的指定的列名 |
initialValue | 默认情况下,JPA 持续性提供程序将所有主键值的起始值设置为 0 |
allocationSize | 分配大小,指主键增长步长。这里指定为1,则意思是主键每次增长为1 |
UniqueConstraint | 默认值:JPA 持续性提供程序假设主键生成器表中任何列均没有唯一约束。若设置需uniqueContraints配合 |
1、先创建保存主键的数据表,并插入初始数据(与JPA方式对比,使用一种即可)
-- 创建表,插入一条信息(也可以不插入,使用JPA建表和初始化数据)
drop table if exists tb_generator;
create table tb_generator (
pk_name varchar(50) not null,
pk_value int(50) not null
) engine = innodb;
INSERT INTO tb_generator (pk_name, pk_value) VALUES ('table_id', 10);
-- tb_generator表初始数据
mysql> select * from tb_generator;
+----------+----------+
| pk_name | pk_value |
+----------+----------+
| table_id | 10 |
+----------+----------+
1 row in set (0.04 sec)
注意:这个表可以给无数的表作为主键表,现在只插入了一数据,这一条数据只是为一个表做主键而已。需要为其他表作为主键表著需要插入一行数据即可。需要保证 table、pkColumnName、valueColumnName 三个属性值相同就可以了
2、使用 GenerationType.TABLE 主键策略:
@Data
@Entity
public class TablePrimarykey {
@Id
@TableGenerator(
name = "id_generator", // 定义一个主键生成器的名称,GeneratedValue会引用
table = "tb_generator", // 表示表生成策略所持久化的表名(如果DB没有tb_generator表会自动创建该表,那么可以设置initialValue)
pkColumnName = "pk_name", // 在持久化表中该主键生成策略所对应键值的名称(列)
pkColumnValue = "table_id", // 主键操作的内容字段(pkColumnName列的值)
valueColumnName = "pk_value", // 表示在持久化表中该生成策略所对应的主键(列)
allocationSize = 10) // 每次增长的步长
@GeneratedValue(strategy = GenerationType.TABLE, generator = "id_generator")
private Long id;
private String name;
}
3、测试代码:
public class TablePrimaryKeyTest {
/**
* 运行之前,修改hibernate.hbm2ddl.auto=update
* 多执行几次然后查看数据库中的数据,
* 如果是刚创建表那么第一条数据是不会按照规则生成,从第二条数据开始查看
*/
@Test
public void testTableId(){
EntityManager entityManager = JpaUtils.getEntityManager();
entityManager.getTransaction().begin();
entityManager.persist(new TablePrimaryKey());// 保存数据,按照TABLE主键策略生成主键
entityManager.getTransaction().commit();
entityManager.close();
}
}
4、查看日志:
Hibernate:
select
tbl.pk_value
from
tb_generator tbl
where
tbl.pk_name=? for update
Hibernate:
update
tb_generator
set
pk_value=?
where
pk_value=?
and pk_name=?
Hibernate:
insert
into
tb_primary_key
(name, id)
values
(?, ?)
5、查看数据库表信息:
mysql> use hibernate_jpa;
Database changed
mysql> select * from tb_generator;
+----+----------+----------+
| id | pk_name | pk_value |
+----+----------+----------+
| 1 | table_id | 40 |
+----+----------+----------+
1 row in set (0.01 sec)
mysql> select * from tb_primary_key;
+----+------+
| id | name |
+----+------+
| 2 | NULL |
| 12 | NULL |
| 22 | NULL |
+----+------+
3 rows in set (0.02 sec)
5、自定义主键生成器
@GenericGenerator 注解(是 Hibernate 自定义主键生成器,可以直接引用内置主键策略) 该策略一般会 @GeneratedValue 和 @GenericGenerator 同时使用,并且@GeneratedValue注解中的”generator”属性要与@GenericGenerator注解中name属性一致,strategy属性表示hibernate的主键生成策略。
(换种方式解释:Hibernate 同时对 JPA 进行了扩展,在 @GeneratedValue 中指定 generator,然后用 @GenericGenerator 指定策略来维护主键)
@Id
@GeneratedValue(generator = "myGenerator") // 使用了generator默认可以不指定strategy
@GenericGenerator(name = "myGenerator", strategy = "uuid") // 使用uuid的生成策略
private String Id;
@GenericGenerator 注解的定义(用来指定序列的相关信息)
@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(GenericGenerators.class)
public @interface GenericGenerator {
String name(); // @GeneratedValue 中 generator 的值
String strategy(); // 生成器名称
Parameter[] parameters() default {}; // 生成器的参数
}
- name:指定生成器名称。会在@GeneratedValue中进行引用
- strategy:指定具体生成器的类名
- parameters:得到strategy指定的具体生成器所用到的参数(可选参数)
通过查看 Hibernate 的源码查看内置主键策略,可以发现strategy
有14个内置主键策略选项:
public DefaultIdentifierGeneratorFactory() {
this.register("uuid2", UUIDGenerator.class);
this.register("guid", GUIDGenerator.class);
this.register("uuid", UUIDHexGenerator.class);
this.register("uuid.hex", UUIDHexGenerator.class);
this.register("assigned", Assigned.class);
this.register("identity", IdentityGenerator.class);
this.register("select", SelectGenerator.class);
this.register("sequence", SequenceStyleGenerator.class);
this.register("seqhilo", SequenceHiLoGenerator.class);
this.register("increment", IncrementGenerator.class);
this.register("foreign", ForeignGenerator.class);
this.register("sequence-identity", SequenceIdentityGenerator.class);
this.register("enhanced-sequence", SequenceStyleGenerator.class);
this.register("enhanced-table", TableGenerator.class);
}
列出几个 Hibernate 比较常用的生成策略:
- native:对于Oracle采用Sequence方式,对于MySQL和SQL Server采用identity(主键自增),native就是将主键的生成交由数据库完成,hibernate不管
- uuid:采用128位的uuid算法生成主键,uuid被编码为一个32位16进制数字的字符串。占用空间大(字符串类型)
- assigned:在插入数据的时候主键由程序处理(即程序员手动指定)这是元素没有指定时的默认生成策略。等同AUTO
- identity:使用SQL Server和MySQL自增字段,Oracle不支持主键自增,要设定Sequence(MySQL 和 SQL Server中很常用) 等于 JPA 的 INDENTITY
- increment:插入数据的时候 Hibernate 会给主键添加一个自增的主键,但是一个 Hibernate 实例就维护一个计数器,所以多实例运行时不能使用这个
@Id
@GeneratedValue(generator = "IDGenerator")
@GenericGenerator(name = "IDGenerator", strategy = "identity")
// 等价于JPA中的IDENTITY策略
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
@Id
@GeneratedValue(generator="paymentableGenerator")
@GenericGenerator(name="paymentableGenerator", strategy="assigned")
// 等价于JPA中的AUTO策略
@Id
@GeneratedValue(GenerationType.AUTO)
// uuid策略的使用,用在Oracle数据库的主键生成上,它会根据内部程序计算出32位长度的唯一id,此时保存对象可以不设置id字段的值
// 如果不用如下2个注解生成,也可以用代码手动生成UUID:`xx.setId(UUID.randomUUID().toString());
@GeneratedValue(generator = "systemUUID")
@GenericGenerator(name = "systemUUID", strategy = "uuid")
@Id
private String id; // 注意类型必须为String
内置主键策略Hibernate—14个 内置主键策略详解:
-
uuid2:
IdentifierGenerator 的实现类是 UUIDGenerator,具体由 UUIDGenerationStrategy 策略负责生成,它有两个实现 StandardRandomStrategy 和 CustomVersionOneStrategy,他们都是使用j ava.util.UUID 的 api 生成主键的。
StandardRandomStrategy 最终由 UUID.randomUUID(); 生成;
CustomVersionOneStrategy 则采用版本号与位运算通过构造函数 new UUID(mostSignificantBits,leastSignificantBits); 生成。
特点是:不需要和数据库交互,可根据RFC4122定义的5中变量控制具体的生成策略(因为符合RFC4122定义,所以避免了警告信息) -
guid:
IdentifierGenerator 的实现类是 GUIDGenerator,通过 session.getFactory().getDialect().getSelectGUIDString(); 获得各个数据库中的标示字符串.
MySQL 用 select uuid();
Oracle 用 return “select rawtohex(sys_guid()) from dual”;
特点是:需要和数据库进行一次查询才能生成。数据库全局唯一 -
uuid,uuid.hex:
uuid和uuid.hex 两个一个东西。IdentifierGenerator的实现类是UUIDHexGenerator,通过:StringBuffer(36).append(format(getIP())).append(sep).append(format(getJVM())).append(sep).append(format(getHiTime())).append(sep).append(format(getLoTime())).append(sep).append(format(getCount()))生成。
特点:不需要和数据库交互,全网唯一 -
hilo:
IdentifierGenerator 的实现类 TableHiLoGenerator,逻辑较为复杂,通过高低位酸腐生成,但是需要给定表和列作为高值的源。加上本地的地位计算所得。
特点:需要和数据库交互,全数据库唯一,与guid不同的是,在标识符的单个源必须被多个插入访问时可以避免拥堵。 -
assigned:
IdentifierGenerator 的实现类 Assigned,没有生成逻辑,如果为空就抛出异常。
特点:不需要和数据库交互,自己管理主键生成,显示的指定id -
identity:
IdentityGenerator 并没有直接实现 IdentifierGenerator,而是扩展了AbstractPostInsertGenerator,并实现PostInsertIdentifierGenerator。
而 PostInsertIdentifierGenerator 实现了 IdentifierGenerator,通过IdentifierGeneratorHelper类生成。
这个比较特殊,它返回是个常量 “POST_INSERT_INDICATOR”,指在数据库插入后时生成,然后返回数据库生成的id;
还有个常量 “SHORT_CIRCUIT_INDICATOR”,是用外键ForeignGenerator时使用的。
特点:需要和数据库交互,数据插入后返回(反查)id,同一列唯一 -
select:
SelectGenerator 扩展了 AbstractPostInsertGenerator 实现了 Configurable 接口,而 AbstractPostInsertGenerator 实现了 PostInsertIdentifierGenerator。所以具有和identity类似的行为,有数据库触发器生成。
特点:需要和数据库交互 -
sequence:
SequenceGenerator 实现了 PersistentIdentifierGenerator 接口,和 Configurable 接口。
PersistentIdentifierGenerator 接口扩展 IdentifierGenerator 接口,通过不同的数据库,获取不同的取值语句 dialect.getSequenceNextValString(sequenceName); 然后进行查询,缓存到IntegralDataTypeHolder中,通过 generateHolder( session ).makeValue(); 获得。
特点:需要和数据库交互(但不是每次都是)。sequence唯一 -
seqhilo:
seqhilo,扩展了 SequenceGenerator, 处理逻辑和 hilo 相同,值不过是使用一个具名的数据库序列来生成高值部分。
特点:需要和数据库交互,全数据库唯一,与guid不同的是,在标识符的单个源必须被多个插入访问时可以避免拥堵 -
increment:
IdentifierGenerator 的实现类 IncrementGenerator,并实现了 Configurable 接口。数据库启动时查询表的最大主键列支,并通过 IntegralDataTypeHolder 缓存。插入一条,它自加一。
特点:仅需要首次访问数据库 -
foreign:
IdentifierGenerator 的实现类 ForeignGenerator,通过给定的 entityName 和 propertyName 查询获得值。
特点:需要和数据库访问
6、主键对数据库的支持
数据库类型 | GenerationType.AUTO | GenerationType.IDENTITY | GenerationType.SEQUENCE | GenerationType.TABLE |
---|---|---|---|---|
MySQL | 支持 | 支持 | 不支持 | 支持 |
Oracle | 支持 | 不支持 | 支持 | 支持 |
PostgreSQL | 支持 | 支持 | 支持 | 支持 |
Kingbase | 支持 | 支持 | 支持 | 支持 |
7、主键UUID与数字对比
自增主键:
- 优点
- 数据存储空间小
- 查询效率高
- 缺点:
- 如果数据量过大,会超出自增长的值范围
- 分布式存储的表操作,尤其是在合并的时候操作复杂
- 安全性低,因为是有规律的,如果恶意扒取用户信息会很容易,如果是单据编号使用,竞争对手会容易查询出货量
UUID主键:
- 优点:
- 出现重复的机会少
- 适合大量数据的插入和更新操作,尤其是在高并发和分布式环境下
- 安全性较高
- 缺点:
- 存储空间大(16 byte),因此它将会占用更多的磁盘空间, MySQL官方有明确的建议主键要尽量越短越好,36个字符长度的UUID不符合要求
- 性能降低,对MySQL索引不利:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能会引起数据位置频繁变动,严重影响性能
适用场景:
- 项目是单机版的,并且数据量比较大(百万级)时,用自增长的,此时最好能考虑下安全性,做些安全措施
- 项目是单机版的,并且数据量没那么大,对速度和存储要求不高时,用UUID
- 项目是分布式的,那么首选UUID,分布式一般对速度和存储要求不高
- 项目是分布式的,并且数据量达到千万级别可更高时,对速度和存储有要求时,可以用自增长
8、参考文献 & 鸣谢
- https://blog.csdn.net/weixin_38446891/article/details/109813272
- https://www.cnblogs.com/badtree/articles/10189769.html
- https://blog.csdn.net/weixin_37878255/article/details/102628997