Java 无约束异常处理的Hibernate线程安全幂等元upsert?

Java 无约束异常处理的Hibernate线程安全幂等元upsert?,java,hibernate,jpa,Java,Hibernate,Jpa,我有一些执行UPSERT的代码,也称为。我想清理这段代码,特别是,我想摆脱异常处理,并为这样一个简单的操作减少代码的总体冗长性和纯粹的复杂性。要求插入每个项目,除非已经存在: public void batchInsert(IncomingItem[] items) { try(Session session = sessionFactory.openSession()) { batchInsert(session, items); } catch(Per

我有一些执行UPSERT的代码,也称为。我想清理这段代码,特别是,我想摆脱异常处理,并为这样一个简单的操作减少代码的总体冗长性和纯粹的复杂性。要求插入每个项目,除非已经存在:

public void batchInsert(IncomingItem[] items) {
    try(Session session = sessionFactory.openSession()) {
        batchInsert(session, items);
    }
    catch(PersistenceException e) {
        if(e.getCause() instanceof ConstraintViolationException) {
            logger.warn("attempting to recover from constraint violation");
            DateTimeFormatter dbFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
            items = Arrays.stream(items).filter(item -> {
                int n = db.queryForObject("select count(*) from rets where source = ? and systemid = ? and updtdate = ?::timestamp",
                        Integer.class,
                        item.getSource().name(), item.getSystemID(), 
                        dbFormat.format(item.getUpdtDateObj()));
                if(n != 0) {
                    logger.warn("REMOVED DUPLICATE: " +
                            item.getSource() + " " + item.getSystemID() + " " + item.getUpdtDate());
                    return false;
                }
                else {
                    return true; // keep
                }
            }).toArray(IncomingItem[]::new);
            try(Session session = sessionFactory.openSession()) {
                batchInsert(session, items);
            }
        }
    }
}
SO的初步搜索结果不令人满意:

  • -概念上类似但更简单的场景,不考虑多线程或多处理
  • 更好的是,通过使用
    @SQLInsert
    注释将原子性推送到数据库,从而消除竞争条件;不幸的是,此解决方案太容易出错,难以在更广泛的表上使用,并且在不断发展的应用程序中需要大量维护
  • 与上述问题非常相似,答案相似
  • 和上面一样,答案提到了
    merge()
    ,这在单线程时是可以的
  • 类似的问题,但选择的答案是不正确的,使用存储过程
  • 又是一个非常幼稚、面向单线程的问题和答案
在标记为重复的问题中,我注意到以下有趣的评论:

这是一条死胡同,因为我真的不理解这条评论,尽管它听起来像是一个聪明的解决方案,并提到“实际相同的SQL语句”

另一个有希望的方法是:

冲突时不执行任何操作/重复密钥更新时

两个主要的开源数据库都支持将幂等性向下推到数据库的机制。下面的示例使用PostgreSQL语法,但可以轻松地适用于MySQL

通过遵循和中的想法,我实现了:

import org.hibernate.resource.jdbc.spi.StatementInspector;

@SuppressWarnings("serial")
public class IdempotentInspector implements StatementInspector {

    @Override
    public String inspect(String sql) {
        if(sql.startsWith("insert into rets")) {
            sql += " ON CONFLICT DO NOTHING";
        }
        return sql;
    }

}
有财产

        <prop key="hibernate.session_factory.statement_inspector">com.myapp.IdempotentInspector</prop>
com.myapp.IdempotentInspector
不幸的是,当遇到重复时,这会导致以下错误:

原因: org.springframework.orm.hibernate5.HibernateOptimisticLockingFailureException: 批更新从更新[0]返回了意外的行计数;实际行 计数:0;预期:1;嵌套异常是 org.hibernate.StaleStateException:批处理更新返回了意外结果 来自更新的行计数[0];实际行数:0;预期:1

这是有道理的,如果您考虑一下封面下发生了什么:CONFLICT DO NOTHING上的
导致插入零行,但预期插入一行

是否有一种解决方案可以启用线程安全的无异常并发幂等插入,而不需要手动定义整个SQL insert语句,以便由Hibernate执行?

值得一提的是,我觉得将dupcheck向下推到数据库的方法是找到合适解决方案的途径

澄清
batchInsert
方法使用的
IncomingItem
对象来自一个记录不可变的系统。在这种特殊情况下,尽管可能会丢失第n次更新,但冲突中的
不执行任何操作
的行为与UPSERT相同。

根据您的帖子,我假设source、systemid和updtdate是唯一的密钥。 基于此。我会的

  • 通过一个查询检索IncomingItem的列表。(我假设此数据库中没有100万条记录)
  • 将唯一键与列表进行比较,并保留要插入的唯一键
  • 保存项目
一些伪代码:

public void batchInsert(IncomingItem[] items) {
    //get all IncomingItem from the DB
    List<IncomingItem> incomingItems = //DB query findAll;
    List<IncomingItem> incomingItemsToSave = new ArrayList<>();
    //check your duplicates!
    for(IncomingItem incomingItem : incomingItems){
        Arrays.stream(items).filter(item -> {
            //compare unique key
            // ...  code here ...
            if(!same unique key){
                incomingItemsToSave.add(item);
            }
        });
    }

    try(Session session = sessionFactory.openSession()) {
        batchInsert(session, incomingItemsToSave);
    }
    catch(PersistenceException e) {

    }
}
public void批插入(IncomingItem[]项){
//从数据库获取所有收入项目
List incomingItems=//DB query findAll;
List incomingItemsToSave=new ArrayList();
//检查你的副本!
对于(收入项目收入项目:收入项目){
Arrays.stream(项目).filter(项目->{
//比较唯一键
//…代码在这里。。。
如果(!相同的唯一键){
incomingItemsToSave.add(项目);
}
});
}
try(Session Session=sessionFactory.openSession()){
batchInsert(会话,incomingItemsToSave);
}
捕获(持久异常e){
}
}

简短回答-Hibernate不支持开箱即用(由中的Hibernate专家确认)。也许您可以使用您已经描述的机制在某些场景中使其在某种程度上工作,但在我看来,直接使用本机查询是实现此目的最直接的方法

更长的答案是,考虑到Hibernate的所有方面,我想很难支持它,例如:

  • 对于发现重复的实例,应该如何处理,因为它们应该在持久化后进行管理?将它们合并到持久性上下文中
  • 如何处理已经持久化的关联,对它们应用哪些级联操作(persist/merge/something\u new;或者现在做出决定是否太晚了)
  • 数据库是否从upsert操作返回足够的信息以覆盖所有用例(跳过的行;在批插入模式下不跳过的生成键,等等)
  • 关于
    @Audit
    -ed实体,它们是创建的还是更新的,如果更新了,发生了什么变化
  • 或者版本控制和乐观锁定(根据定义,您实际上希望在这种情况下出现异常)
即使Hibernate以某种方式支持它,如果有太多的警告需要注意和考虑,我也不确定我是否会使用该功能

因此,我遵循的经验法则是:

  • 对于简单场景(大多数情况下):持续+重试。根据您在项目中使用的框架,可以使用类似AOP的方法(注释、自定义拦截器和类似方法)全局配置特定错误的重试(通过异常类型或类似方法),这是一种很好的做法,尤其是在分布式环境中
  • 对于复杂场景和性能密集型操作(尤其是