JPA 事务回滚重试和恢复:将实体与自动递增的@Version 合并

Posted

技术标签:

【中文标题】JPA 事务回滚重试和恢复:将实体与自动递增的@Version 合并【英文标题】:JPA transaction rollback retry and recovery: merging entity with auto-incremented @Version 【发布时间】:2011-09-22 21:03:23 【问题描述】:

我想在交易失败后恢复。

现在,当然,在任何回滚之后,所有实体都会分离并且实体管理器会关闭。但是,UI 仍然包含分离的实体。显然我们不能直接丢弃用户的更改,所以我们想让他们重试(修复突出显示的验证错误,然后再次单击按钮)。

关注Java Persistence WikiBook,

错误处理的一种方法是在提交失败后为每个托管对象调用合并到一个新的EntityManager,然后尝试提交新的EntityManager。一个问题可能是任何已分配的 id 或已分配或递增的乐观锁版本可能需要重置。此外,如果原来的 EntityManager 被 EXTENDED 扩展,任何正在使用的对象仍然会被分离,并且需要重新设置。

这个选项起初看起来很简单,直到我们不可避免地遇到那些预期的问题。某些服务可能会因各种原因触发刷新,这会增加 DB(已回滚)和 Java 实体(未回滚)中的 @Version 字段。下一个“保存”调用merge,它会抛出一个意外的OptimisticLockException

是否有可靠的方法来“回滚”Java 实体 bean 中的版本字段?

好的,接缝很硬。我们有自己的@Versions 级联实体,所以手动操作似乎很脆弱。 (无论如何,我们如何才能可靠地知道原始(持久)版本?无法查询,因为其他用户可能同时成功更新实体;查询当前版本可能会破坏 oplocking!)

另一个更复杂的错误处理方法是始终使用非事务性 EntityManager。当需要提交时,会创建一个新的 EntityManager,将非事务性对象合并到其中,然后提交新的 EntityManager。如果提交失败,只有新的EntityManager的状态可能不一致,原来的EntityManager不受影响。这可以让问题得到纠正,并将 EntityManager 重新合并到另一个新的 EntityManager 中。如果提交成功,任何提交更改都可以合并回原始 EntityManager,然后可以继续正常使用。此解决方案需要相当多的开销,因此应仅在确实需要错误处理且 JPA 提供程序没有提供替代方案时使用。

这似乎合乎逻辑。 有没有人有使用两个 EntityManager(尤其是 Spring)实现这种恢复的经验?在尝试之前我应该​​注意哪些陷阱? 似乎每个服务和 DAO 现在都必须了解这两个实体管理器(对于 Spring,今天它们几乎与持久层无关)。 DAO 的“查找”操作使用一个 EM; 'update' 使用另一个。或者有单独的“读”和“写”DAO。哎哟。

我考虑过的其他选项包括:

在 UI 中使用 DTO,因此自动递增不会影响任何内容。丑陋。 将对merge 的调用移至任何组合操作的结束。仅在所有验证和状态更新成功后才附加实体。 “保存”服务不再merge(仅验证)似乎很奇怪。实际上,UI 将负责调用 DAO!这听起来很不寻常吗?

建议?谢谢 :-)

更新 我的架构包括:

由 UI (JSF) 更新的分离实体 实体 ID 不是自动生成的(预分配的 UUID 和/或业务密钥) 实体具有自动递增的 @Version 字段以进行 oplocking “保存”服务验证,调用em.merge(JPA over Hibernate) “流程”服务验证、应用业务逻辑、更新实体状态 可以组合服务。一个 UI 按钮就可以了
    (关于 UI 控制器的 Spring @Transactional 建议:开始) 保存 流程 1 流程 2 (@Transactional: 提交)
任何服务都可能抛出 validation exception (JSR 303),它会按预期回滚(消息显示在 UI 中)

【问题讨论】:

如果您的 UI 包含分离实体,那么这些实体扮演与 DTO 相同的角色:merge 仅将分离实体的状态复制到附加实体,分离实体保持原样(即它们的版本字段不应修改,无论事务是否已提交)。 @JB,马塞尔偷了你的答案。在那里发表评论 在这种情况下,使用EntityManager.merge() 是危险的。请参阅 JPA 2.1,“3.3.3 事务回滚”部分。它说:“[..] 版本属性的状态和生成的状态(例如,生成的主键)可能不一致。”正如本节所指出的,将这样一个分离的实体传递给合并操作“可能会失败”。 “3.2.7.1 合并分离实体状态”部分有解释:“实体使用的任何版本列都必须在合并操作期间由持久性运行时实现检查 [..]”。 【参考方案1】:

只能支持JB Nizet的评论:merge只复制分离实体的状态。

【讨论】:

如果 UI 的 version 字段没有更新,那么 oplocking 是不是坏了?【参考方案2】:

显然我们不能就扔掉用户的更改

如果用户正在尝试更新,而其他人已经对其进行了更改,那么用户最好刷新副本并再次进行更改,不是吗?

【讨论】:

【参考方案3】:

您提出了两个不同的问题,然后提出了一般性的建议。我会接受建议(会将此放在评论中,但SO不会让我发表评论......)

在 UI 中使用 DTO(或任何仅 UI 的数据结构)并不难看 - 它在架构上是合理的。我同意 JB Nizet 的观点,即非事务性 EM 方法本质上是将您的实体变成了 DTO。如果你必须在你的 UI 中使用实体(我意识到 Spring 鼓励这样做),这就是要走的路。

根据我的经验,人们希望将实体用作 UI 暂存存储的主要原因是访问其中的复杂验证逻辑。您可能还需要考虑进行重构,以使该逻辑无需调用完整的域模型即可访问。

【讨论】:

【参考方案4】:

由于EntityManager.merge() 创建了传递实体的副本,解决方案似乎非常简单:在 UI(JSF bean)中保持对传递实体的引用,直到事务成功完成,然后才用复制的实体更新引用:

@ManagedBean
@SomeScoped
public class EntityBean

    @EJB
    private PersistenceService service;

    private Object entity;

    public void update()
    
        try
        
            // service.update() is a CMT method
            Object updatedEntity = service.update(entity);

            // if we are here transaction has completed successfully, so update the reference 
            entity = updatedEntity;
        
        catch(Exception e)
        
            // if catched, entity has not been modified (just like transaction never happened)
            // now handle exception (log, add a FacesMessage, rethrow, ...)
        
    

    ...

您可以重新渲染 UI,UIComponents 将使用用户在提交上一个请求之前填写的完全相同的实体。

这样您就不必担心重置@Ids、@Versions,也不必担心任何其他更改的字段或属性,包括级联关联,也不必使用多个EntityManagers,也不必使用扩展的持久性上下文。

不过,这个简单的例子有一些限制:

    永远不要在“通过”的实体上使用EntityManager.persist,因为persist 不会执行复制。您必须始终使用EntityManager.merge,但在某些情况下这根本不可能。

    传递的实体必须处于“TRANSIENT”或“DETACHED”状态,EntityManager.merge 才能创建副本(否则返回传递的实体,不执行复制)。这意味着严格禁止使用扩展持久性上下文或OpenSessionInViewFilter 及其派生词。

    使用子事务实现服务组合时应特别小心:当子事务成功完成但父事务回滚时,您可能会以错误的实体结束。

请注意,我不知道 Spring 是如何工作的,所以所有这些可能根本不适用。

【讨论】:

以上是关于JPA 事务回滚重试和恢复:将实体与自动递增的@Version 合并的主要内容,如果未能解决你的问题,请参考以下文章

Spring Data:重试时回滚事务

Spring Data:重试时回滚事务

系统故障时自动恢复进程将使用事务日志前滚所有已提交的事务,并回滚任何未完成的事务。啥是前滚和回滚

JPA中事务回滚的问题

AWS 中的错误重试和指数退避

Spring JPA Junit 关闭自动回滚