首页 > 解决方案 > Hibernate+SQLServer / 批量只插入新记录

问题描述

我正在尝试使用 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 抛出 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 忽略重复条目的表中?

谢谢

标签: javasql-serverhibernatebatch-insert

解决方案


为了获得更好的性能,我更喜欢使用 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();

推荐阅读