日常搬磚踩坑系列——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的生態太強大,知道怎么用的同時,還是知道其實現原理。
