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的生態太強大,知道怎么用的同時,還是知道其實現原理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM