zoukankan      html  css  js  c++  java
  • 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的生态太强大,知道怎么用的同时,还是知道其实现原理。

  • 相关阅读:
    js学习笔记之标准库
    js学习笔记
    html5学习
    C#调用win32 api程序实例
    javascript计算字符串中出现最多的字符和个数
    javascript中的promise和deferred:实践(二)
    jquery animate 详解
    jquery animate stop函数解析
    javascript继承的写法
    css3 改变滚动条样式
  • 原文地址:https://www.cnblogs.com/sunjingwu/p/11700403.html
Copyright © 2011-2022 走看看