我有一些执行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的搜索不能令人满意:
merge()
,当单线程时可以使用在被标记为重复项的问题How to do ON DUPLICATE KEY UPDATE in Spring Data JPA?中,我注意到了这个有趣的评论:
那是一个死胡同,尽管听起来听起来像是一个聪明的解决方案,但我确实不理解该评论,而且提到了“实际上是相同的SQL语句”。
另一种有前途的方法是:Hibernate and Spring modify query Before Submitting to DB
不冲突 / 不重复密钥更新
两个主要的开放源数据库均支持将幂等性向下推至数据库的机制。下面的示例使用PostgreSQL语法,但可以轻松地适用于MySQL。
通过遵循Hibernate and Spring modify query Before Submitting to DB,Hooking into Hibernate's query generation和How 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次更新可能会丢失。
答案 0 :(得分:3)
简短的回答-Hibernate不开箱即用(由this blog post中的Hibernate专家确认)。也许您可以使用已经描述的机制在某些情况下使其在某种程度上起作用,但是为此目的,直接使用本机查询对我而言是最直接的方法。
更长的答案是,考虑到我认为的Hibernate的各个方面,很难支持它,例如:
@Audit
版本的实体如何创建或更新(如果已更新)?即使Hibernate以某种方式支持它,如果有太多需要注意和考虑的警告,我不确定是否会使用该功能。
所以,我遵循的经验法则是:
答案 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是唯一键。 基于此。我会
一些伪代码:
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) {
}
}