Hibernate+SQLServer / 批量只插入新记录

Posted

技术标签:

【中文标题】Hibernate+SQLServer / 批量只插入新记录【英文标题】:Hibernate+SQLServer / Batch insert only the new records 【发布时间】:2019-06-19 11:29:38 【问题描述】:

我正在尝试使用 Hibernate 4.3 和 SQL-Server 2014 仅对尚未存储的实体执行批量插入到表中的操作。 我创建了一个简单的表,主键定义为忽略重复键

create table items 
(
    itemid uniqueidentifier not null, 
    itemname nvarchar(30) not null, 
)
alter table items add constraint items_pk primary key ( itemid ) with ( ignore_dup_key = on );

尝试通过StatelessSession insert方法进行批量插入,如果一个或多个实体已经存入数据库表,批量插入可能会失败:Hibernate throws a StaleStateException:

org.hibernate.StaleStateException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
    at org.hibernate.jdbc.Expectations$BasicExpectation.checkBatched(Expectations.java:81)
    at org.hibernate.jdbc.Expectations$BasicExpectation.verifyOutcome(Expectations.java:73)
    at org.hibernate.engine.jdbc.batch.internal.NonBatchingBatch.addToBatch(NonBatchingBatch.java:63)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3124)
    at org.hibernate.persister.entity.AbstractEntityPersister.insert(AbstractEntityPersister.java:3581)
    at org.hibernate.internal.StatelessSessionImpl.insert(StatelessSessionImpl.java:144)
    at org.hibernate.internal.StatelessSessionImpl.insert(StatelessSessionImpl.java:123)
    at it.test.testingestion.HibernateStatelessSessionPersisterImpl.persistData(HibernateStatelessSessionPersisterImpl.java:18)
    at it.test.testingestion.App.main(App.java:76)

当批处理语句完成时,由于忽略重复键,Hibernate 会检查返回的行数,该行数与预期不同。

使用 JDBC,使用准备好的语句执行批量插入,已存储到目标表中的实体被跳过,但新实体被正确保存。

如何配置 Hibernate 以执行批量插入而忽略现有数据或不检查受影响的行?

非常感谢

更新 #1

作为解决方法,即使发生重复插入,为了强制受影响的行数,我创建了以下 Hibernate 拦截器:

public class CustomInterceptor extends EmptyInterceptor 

    private static final long serialVersionUID = -8022068618978801796L;

    private String getTemporaryTableName() 
        String currentThreadName = Thread.currentThread().getName();
        return "##" + currentThreadName.replaceAll("[^A-Za-z0-9\\_]", "");
    

    private void createTemporaryTable(Connection connection) 
        String tempTableName = this.getTemporaryTableName();
        String commandText = String.format("if (object_id('tempdb.dbo.%s') is null) begin create table [%s] ( dummyfield int ); insert into %s ( dummyfield ) values ( 0 ) end ", tempTableName, tempTableName, tempTableName);
        try (PreparedStatement statement = connection.prepareStatement(commandText)) 
            statement.execute();
            connection.commit();
         catch (SQLException e) 
            throw new RuntimeException(String.format("An error has been occurred trying to create the temporary table %s", tempTableName), e);
        
    

    public CustomInterceptor(Connection connection) 
        this.createTemporaryTable(connection);
    

    @Override
    public String onPrepareStatement(String sql) 
        int ps = sql.toLowerCase().indexOf("insert into ");
        if (ps == 0) 
            String tableName = this.getTemporaryTableName();
            return sql + "; if (@@rowcount = 0) update [" + tableName + "] set dummyfield = 1"; 
        
        return super.onPrepareStatement(sql);
    


拦截器在创建新实例时创建一个插入新记录的新临时表。 当插入语句被拦截时,如果插入语句没有影响任何行,则执行保存到实例化临时表中的记录的更新:如果插入重复实体并且没有 StatelessSessionImpl 异常,这会欺骗 Hibernate 关于返回的行事件扔了。

显然,该技巧的缺点是对未插入到表中的每一行执行额外更新的成本。

有谁知道更好的方法,不影响插入性能,将实体插入到使用 Hibernate 忽略重复条目的表中?

谢谢

【问题讨论】:

【参考方案1】:

为了更好的性能,我更喜欢使用 JDBCBatchUpdate

方法一:

当您过滤新记录时,记录数将不受限制。因此,您可以在实体层中指定关联映射,并可以执行 Hibernate 批量插入或 JDBC 批量更新。

方法 2: 使用原生 SQL 查询

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();
//get Connction from Session
session.doWork(new Work() 
       @Override
       public void execute(Connection conn) throws SQLException 
          PreparedStatement pstmt = null;
          try
           String sqlInsert = "insert into tbl(name) values (?) ";
           pstmt = conn.prepareStatement(sqlInsert );
           int i=0;
           for(String name : list)
               pstmt .setString(1, name);
               pstmt .addBatch();

               //20 : JDBC batch size
             if ( i % 20 == 0 )  
                pstmt .executeBatch();
              
              i++;
           
           pstmt .executeBatch();
         
         finally
           pstmt .close();
                                         
     
);
tx.commit();
session.close();

【讨论】:

非常感谢您的回答。但是,在大型实体模型场景中,使用 JDBC 方法,意味着对 Hibernate 提供的持久层进行重新编码,包括 listeners、evners 等。出于性能原因是否也可以使用本机 SQL-Bulkcopy API,但问题与使用 JDBC 方法相同。

以上是关于Hibernate+SQLServer / 批量只插入新记录的主要内容,如果未能解决你的问题,请参考以下文章

怎样解决hibernate中一级缓存导致数据不能刷新

sqlserver数据库批量插入-SqlBulkCopy

sqlServerMySql批量操作插件

Grails批量处理锁定在桌子上

Hibernate一级缓存

spring mvc+spring + hibernate 整合