Hibernate 主键生成策略——Duplicate entry '1024' for key 'PRIMARY'
日常搬砖踩坑系列——Hibernate主键生成策略,主键冲突
项目开发完毕,前后端接口联调;前端童鞋反应新增接口偶尔会报错,经过查看后端服务日志:java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '1024' for key 'PRIMARY',明显是写入数据主键冲突,一个新增接口并且数据表的主键是自增的,怎么会主键冲突呢?
还原场景
接口联调,基本是在dev环境,有些时候为了方便开发人员也会本地启动服务连接同一个数据库;前端在切换api地址测试接口时会报错。
分析原因
因为主键是自增的,并且新增接口没有指定id(依靠数据库自增),居然会出现主键冲突错误,难道新增时id被指定了而且是开发人员不知情,貌似找到了原因,因为项目中实体类指定了主键生成策略;代码如下:
@Entity
@Table(name = "user")
public class User {
@Id
@GenericGenerator(name = "autoId", strategy = "increment")
@GeneratedValue(generator = "autoId")
private Integer id;
private String name;
}
验证:将项目SQL进行日志输出(jpa.show-sql=true),strategy = "increment"
Hibernate: select max(id) from user
Hibernate: insert into user (name, id) values (?, ?)
果然新增时指定了主键id,并且select max(id) from user
google了一番,发现当主键生成策略指定为“increment”,插入数据的时候hibernate会通过自己维护的主键给主键赋值,相当于hibernate实例就维护一个计数器作为主键,所以在多个实例(集群)运行的时候不能使用这个生成策略;找到问题的根源了,解决办法把“increment”改为“native”或“identity”,推荐“native”,不需要hibernate维护主键id,依靠数据库完成这个任务,问题得以解决。
验证:将项目SQL进行日志输出(jpa.show-sql=true),strategy = "native"
Hibernate: insert into user (name) values (?)
当strategy = "increment",第一次会将表中最大id查询出来,hibernate维护这个id(并且多个开发启动多个服务实例各自维护id),不依靠底层数据库才导致主键冲突。
拓展知识
那么主键生成策略多有那些呢?
GeneratedValue
@Target({METHOD, FIELD})
@Retention(RUNTIME)
public @interface GeneratedValue {
// 生成策略
GenerationType strategy() default AUTO;
// 生成器名称
String generator() default "";
}
public enum GenerationType {
// 使用一个特定的数据库表格来保存主键
TABLE,
// 根据底层数据库的序列来生成主键,条件是数据库支持序列(Oracle)。
SEQUENCE,
// 主键由数据库自动生成(主要是自动增长型,MySQL、SQL Server)
IDENTITY,
// 主键由程序控制
AUTO
}
- TableGenerator 表生成器, GeneratedValue的strategy为GenerationType.TABLE
将当前主键的值单独保存到数据库的一张表里去,主键的值每次都是从该表中查询获得,适用于任何数据库,不必担心兼容问题
@Repeatable(TableGenerators.class)
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface TableGenerator {
// 属性表示该生成器的名称,它被引用在@GeneratedValue中设置的“generator”值中
String name();
// 主键保存到数据库的表名
String table() default "";
String catalog() default "";
String schema() default "";
// 表里用来保存主键名字的字段
String pkColumnName() default "";
// 表里用来保存主键值的字段
String valueColumnName() default "";
// 表里名字字段对应的值
String pkColumnValue() default "";
int initialValue() default 0;
// //自动增长,设置为1
int allocationSize() default 50;
UniqueConstraint[] uniqueConstraints() default {};
Index[] indexes() default {};
}
@Data
@Entity
@Table(name = "user")
public class User {
@Id
@GeneratedValue(generator="tableGenerator",strategy = GenerationType.TABLE)
@TableGenerator(name="tableGenerator",
table = "id_table",
pkColumnName = "id_name",
valueColumnName = "id_value",
pkColumnValue = "user_id",
initialValue = 1,
allocationSize = 1)
private Integer id;
private String name;
}
需要id_table表存放主键
id | id_name | id_value |
---|---|---|
1 | user_id | 1 |
新增数据时,需要从id_table将id_value查询出来,写入user表,更新id_table表id_value,流程日志如下:
Hibernate: select tbl.id_value from id_table tbl where tbl.id_name=? for update
Hibernate: update id_table set id_value=? where id_value=? and id_name=?
Hibernate: insert into user (name, id) values (?, ?)
- SequenceGenerator 序列生成器,条件是数据库支持序列(Oracle);GeneratedValue的strategy为GenerationType.SEQUENCE
@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface SequenceGenerator {
// 属性表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中
String name();
// 属性表示生成策略用到的数据库序列名称
String sequenceName() default "";
// 表示主键初识值,默认为0
int initialValue() default 0;
// 表示每次主键值增加的大小,例如设置成1,则表示每次创建新记录后自动加1,默认为50
int allocationSize() default 50;
}
// 条件是数据库支持序列(Oracle)
@Id
@GeneratedValue(strategy =GenerationType.SEQUENCE,generator="sequenceGenerator")
@SequenceGenerator(name="sequenceGenerator", sequenceName="sequence_name")
- IDENTITY 主键则由数据库自动维护,使用起来很简单
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
// 等价于
@Id
@GenericGenerator(name = "autoId", strategy = "native")
@GeneratedValue(generator = "autoId")
// 或
@Id
@GenericGenerator(name = "autoId", strategy = "identity")
@GeneratedValue(generator = "autoId")
- AUTO 默认的配置。如果不指定主键生成策略,默认为AUTO,需要配合GenericGenerators使用
// 自定义主键生成策略
@Target({PACKAGE, TYPE, METHOD, FIELD})
@Retention(RUNTIME)
@Repeatable(GenericGenerators.class)
public @interface GenericGenerator {
// 属性表示该表主键生成策略的名称,它被引用在@GeneratedValue中设置的“generator”值中
String name();
// 属性指定具体生成器的类名
String strategy();
// parameters得到strategy指定的具体生成器所用到的参数
Parameter[] parameters() default {};
}
通过DefaultIdentifierGeneratorFactory实现
public DefaultIdentifierGeneratorFactory() {
// 发现此处并没有native,
register( "uuid2", UUIDGenerator.class );
register( "guid", GUIDGenerator.class ); // can be done with UUIDGenerator + strategy
register( "uuid", UUIDHexGenerator.class ); // "deprecated" for new use
register( "uuid.hex", UUIDHexGenerator.class ); // uuid.hex is deprecated
register( "assigned", Assigned.class );
register( "identity", IdentityGenerator.class );
register( "select", SelectGenerator.class );
register( "sequence", SequenceStyleGenerator.class );
register( "seqhilo", SequenceHiLoGenerator.class );
register( "increment", IncrementGenerator.class );
register( "foreign", ForeignGenerator.class );
register( "sequence-identity", SequenceIdentityGenerator.class );
register( "enhanced-sequence", SequenceStyleGenerator.class );
register( "enhanced-table", TableGenerator.class );
}
@Override
public Class getIdentifierGeneratorClass(String strategy) {
if ( "hilo".equals( strategy ) ) {
throw new UnsupportedOperationException( "Support for 'hilo' generator has been removed" );
}
// 在这里
String resolvedStrategy = "native".equals( strategy ) ?
getDialect().getNativeIdentifierGeneratorStrategy() : strategy;
Class generatorClass = generatorStrategyToClassNameMap.get( resolvedStrategy );
常用的生成策略
- increment 插入数据的时候hibernate会给主键添加一个自增的主键,但是一个hibernate实例就维护一个计数器,所以在多个实例运行的时候不能使用这个方法,查看IncrementGenerator实现
public class IncrementGenerator implements IdentifierGenerator, Configurable {
private String sql;
private IntegralDataTypeHolder previousValueHolder;
// 同步方法,保证线程安全
@Override
public synchronized Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {
// 第一次sql!=null,select max
if ( sql != null ) {
initializePreviousValueHolder( session );
}
// 获取id并自增
return previousValueHolder.makeValueThenIncrement();
}
@Override
public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {
// 此处与日志打印的相吻合
sql = "select max(" + column + ") from " + buf.toString();
}
private void initializePreviousValueHolder(SharedSessionContractImplementor session) {
previousValueHolder = IdentifierGeneratorHelper.getIntegralDataTypeHolder( returnClass );
final boolean debugEnabled = LOG.isDebugEnabled();
if ( debugEnabled ) {
LOG.debugf( "Fetching initial value: %s", sql );
}
try {
PreparedStatement st = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql );
try {
ResultSet rs = session.getJdbcCoordinator().getResultSetReturn().extract( st );
try {
if ( rs.next() ) {
previousValueHolder.initialize( rs, 0L ).increment();
}
else {
previousValueHolder.initialize( 1L );
}
// generate 不在select max
sql = null;
if ( debugEnabled ) {
LOG.debugf( "First free id: %s", previousValueHolder.makeValue() );
}
}
}
// 处理维护的id
public final class IdentifierGeneratorHelper {
public Number makeValueThenIncrement() {
final Number result = makeValue();
value = value.add( BigInteger.ONE );
return result;
}
}
- identity 使用SQL Server 和 MySQL 的自增字段,这个方法不能放到 Oracle 中,Oracle 不支持自增字段,要设定sequence(MySQL 和 SQL Server 中很常用)
- sequence 调用数据库的sequence来生成主键,要设定序列名,不然hibernate无法找到(Oracle 中常用)
- native 对于 oracle 采用 Sequence(序列),对于MySQL 和 SQL Server 采用identity(自增主键),native就是将主键的生成工作交由数据库完成,hibernate不管(很常用、推荐)
当把@GenericGenerator注释或去掉,把@GeneratedValue(strategy = GenerationType.IDENTITY)再次测试,日志没有打印select max(id) from user,不需要维护id,这是另一种解决方案。
总结
java.sql.SQLIntegrityConstraintViolationException: Duplicate entry '1024' for key 'PRIMARY' 出现,是因为主键生成策略strategy = "increment",
- strategy = "increment"
- 优点,使用起来比较方便,跨数据库,不许底层数据库支持自增,由hibernate实现自增
- 缺点,hibernate实现自增,即同一个JVM内没有问题,如果服务是集群模式(多JVM),就会出现主键冲突问题
@Id
@GenericGenerator(name = "autoId", strategy = "increment")
@GeneratedValue(generator = "autoId")
// 用下面即可
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
- GenerationType.TABLE同样可已跨数据库,GenerationType.SEQUENCE主要用于oralce、PostgerSQL支持sequence机制的数据库,GenerationType.IDENTITY主要用MySQL、SQL Server等支持主键自增的数据库
Java的生态太强大,知道怎么用的同时,还是知道其实现原理。