没有约束异常处理的Hibernate线程安全幂等向上插补吗?

时间:2019-06-05 02:50:50

标签: java hibernate jpa

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

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的搜索不能令人满意:

在被标记为重复项的问题How to do ON DUPLICATE KEY UPDATE in Spring Data JPA?中,我注意到了这个有趣的评论: enter image description here

那是一个死胡同,尽管听起来听起来像是一个聪明的解决方案,但我确实不理解该评论,而且提到了“实际上是相同的SQL语句”。

另一种有前途的方法是:Hibernate and Spring modify query Before Submitting to DB

不冲突 / 不重复密钥更新

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

通过遵循Hibernate and Spring modify query Before Submitting to DBHooking into Hibernate's query generationHow I can configure StatementInspector in Hibernate?中的想法,我实现了:

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>

不幸的是,这在遇到重复项时导致以下错误:

  

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

考虑到幕后发生的事情,这是有道理的:ON CONFLICT DO NOTHING会导致插入零行,但预计会插入一次。

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

对于它的价值,我认为将dupcheck推送到数据库的方法是寻求正确解决方案的途径。

澄清 IncomingItem方法消耗的batchInsert对象源自记录不变的系统。在这种特殊情况下,ON CONFLICT DO NOTHING的行为与UPSERT相同,尽管第N次更新可能会丢失

3 个答案:

答案 0 :(得分:3)

简短的回答-Hibernate不开箱即用(由this blog post中的Hibernate专家确认)。也许您可以使用已经描述的机制在某些情况下使其在某种程度上起作用,但是为此目的,直接使用本机查询对我而言是最直接的方法。

更长的答案是,考虑到我认为的Hibernate的各个方面,很难支持它,例如:

  • 对于发现重复的实例该怎么办,因为它们应该在持久化之后进行管理?将它们合并到持久性上下文中?
  • 如何处理已经存在的关联,将哪些级联操作应用于它们(持久/合并/ something_new;或者此时做出该决定为时已晚)?
  • 数据库是否从upsert操作返回足够的信息以涵盖所有用例(跳过的行;为批量插入模式中未跳过的键生成的键等)。
  • @Audit版本的实体如何创建或更新(如果已更新)?
  • 还是版本控制和乐观锁定(根据定义,您实际上想在这种情况下例外)?

即使Hibernate以某种方式支持它,如果有太多需要注意和考虑的警告,我不确定是否会使用该功能。

所以,我遵循的经验法则是:

  • 对于简单情况(大多数情况下):坚持+重试。可以使用类似于AOP的方法(注释,自定义拦截器和类似方法)全局配置特定错误(按异常类型或类似类型)的重试,具体取决于您在项目中使用的框架,这是一个好习惯,尤其是在分布式环境中。
  • 对于复杂的场景和性能密集的操作(尤其是在批处理,非常复杂的查询等方面):本机查询可最大程度地利用特定数据库功能。

答案 1 :(得分:0)

请注意,“幂等”与“冲突忽略时”不同。后者可能导致对数据库的第二次写操作被忽略,即使在插入失败时实际上应该执行 update

  

有没有一种解决方案可以启用线程安全的无异常并发   幂等插入

我想说,如果没有RDBMS的特别支持,那么从理论上讲这甚至是不可能的,尤其是“并发”部分。原因是在提交事务之前,数据不会真正被写入,甚至可能不可见。因此,如果在事务A中确定记录不存在并且完成了INSERT,将会发生什么。即使该INSERT对于其他事务来说是立即可见的,但并发事务B仍将确定它应该执行UPDATE。现在,如果以后的事务A遇到导致其回滚的问题该怎么办?事务A的INSERTED数据消失,事务B的UPDATE找不到要更新的记录。

这是“并发”部分通常无法使用的原因之一,因为并非所有RDBMS都支持某种原子UPSERT(或“忽略冲突”)。

但是,似乎您不介意丢失对同一条记录的第二次写入(更新),因为您在谈论幂等,这意味着潜在的UPDATE实际上不会修改记录的数据已经存在。在这种情况下,“无视冲突”确实等同于幂等。

一个(显而易见的)解决方案是(在数据库中)使用一些显式锁进行互斥,即事务A获取该锁,执行该锁,然后再次释放它。事务B尝试获取该锁,但将被阻止直到事务A完成。但是,这将减少或阻止并发,尤其是如果您在一个事务中处理大量记录时。另外,由于RDBMS不了解锁与它所保护的记录之间的关系,因此该锁仅是建议性的,每个客户端都必须采用相同的锁方案。

您说您想“将幂等性推入数据库”。如果这不是严格的要求,则您可以只控制Java代码中的并发性。例如通过使用一些具有并发性的集合,您的代码自动检查并插入将要写入RDBMS的每个数据项的ID。如果ID已在集合中,则跳过该项目,否则插入DB。

答案 2 :(得分:-1)

根据您的帖子,我认为source,systemid和updtdate是唯一键。 基于此。我会

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

一些伪代码:

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) {

    }
}