在 EntityListener 中使用带有 PersistenceContext 的 EJB 时,JPA 尝试在数据库中插入相同的实体两次

Posted

技术标签:

【中文标题】在 EntityListener 中使用带有 PersistenceContext 的 EJB 时,JPA 尝试在数据库中插入相同的实体两次【英文标题】:JPA tries to insert the same entity twice in the database when using EJBs with PersistenceContext in EntityListener 【发布时间】:2017-01-03 20:24:44 【问题描述】:

我想通过组合和一些 JPA 回调方法对我的一些 JPA 实体类进行审计。我目前的方法(缩短)如下所示:

我要审计的每个实体都实现以下简单接口:

public interface Auditable 
    MetaContext getAuditContext();
    void setAuditContext(MetaContext context);

元上下文是另一个保存审计信息的 JPA 实体类:

@Entity
public class MetaContext implements Serializable    
    @Id private Long id;

    private Date whenCreated;
    private Date whenUpdated;   
    @ManyToOne private User whoCreated;    
    @ManyToOne private User whoUpdated;  

这是通过我的 EntityClass 中的组合使用的

@Entity
@EntityListeners(AuditListener.class)
public class MyEntity implements Auditable 
    // ...
    @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST)
    private MetaContext auditContext;

AuditListener 如下所示:

public class AuditService      
    @Inject private SecurityService securityService;    

    @PrePersist
    public void prePersist(Auditable auditable)     
        System.out.println("PrePersist method called");  
        MetaContext context = new MetaContext();
        context.setWhenCreated(new Date());       
        context.setWhoCreated(securityService.getCurrentUser());        
        auditable.setAuditContext(context);                
    

    @PostUpdate
    public void postUpdate(Auditable auditable)      
        System.out.println("PostUpdate method called");    
        MetaContext context = auditable.getAuditContext();
        context.setWhenUpdated(new Date());
        context.setWhoUpdated(securityService.getCurrentUser());        
    

SecurityService 是另一个 EJB,我用它来检索属于执行操作的实际用户的 User 实例。

@Stateless
public class SecurityService 

    @Resource private SessionContext sctx;
    @PersistenceContext private EntityManager em;

    public String getCurrentUserName() 
        Principal principal = sctx == null ? null : sctx.getCallerPrincipal();
        return principal == null ? null : principal.getName();
    

    public User getCurrentUser() 
        String username = getCurrentUserName();
        if (username == null) 
            return null;
         else 
            String jpqlQuery = "SELECT u FROM User u WHERE u.globalId = :name";
            TypedQuery<User> query = em.createQuery(jpqlQuery, User.class);
            query.setParameter("name", username);
            return EJBUtil.getUniqueResult(query.getResultList());
        
    

在每个实体中,@TableGenerator@GeneratedValue 用于创建 ID。


行为: 在新的 MyEntity 实例上调用 EntityManager.persist 时,首先调用我的 EntityListener 的 PrePersist 方法,如预期的那样。日期和用户设置正确。之后调用@PostUpdate 方法。 这似乎是 EclipseLink 特定于 ID 的生成。我注意到之前在我使用不同方法的其他项目中。 但是,现在在 @PostUpdate 方法中再次使用查找当前用户的服务,这似乎导致 JPA 想要插入 @987654334 @ 进入数据库几次,其中的结果违反了该表上的 Primaray Key 约束。如果我删除 PK 约束,则操作成功完成,但结果错误:whenUpdated 设置为当前日期,但 whoUpdated 仍然是null。此外,还记录了一个我不太明白的警告。上面的代码产生以下日志:

Info:   PrePersist method called
Info:   PostUpdate method called
Info:   PostUpdate method called
Warning:   The class RepeatableWriteUnitOfWork is already flushing. The query will be 
    executed without further changes being written to the database.  If the query is 
    conditional upon changed data the changes may not be reflected in the results. 
    Users should issue a flush() call upon completion of the dependent changes and 
    prior to this flush() to ensure correct results.

现在变得更奇怪了:如果我在 @PostUpdate 方法中注释掉 // context.setWhoUpdated(securityService.getCurrentUser()); 行,日志表明回调方法(仅)被调用一次,但 whenUpdated 仍然是 null

如果有任何问题或缺少任何信息,请告诉我,我会更新问题。

如果我根本不使用任何更新回调,我不会发现任何问题。

谁能解释实际问题和/或知道如何解决我的方法?

【问题讨论】:

【参考方案1】:

披露:我对 JPA 有很好的了解,但我从未在 EJB 设置中使用过它。

记录的警告的标识符是nested_entity_manager_flush_not_executed_pre_query_changes_may_be_pending,如果您查看代码RepeatableWriteUnitOfWork(第421 行,EL 2.5.2),您可以看到很多代码被跳过。由于某种原因,UnitOfWork 中的更改被写入了两次。

要记住的一点是,在事务中 EclipseLink 刷新,(默认刷新模式为自动)在执行选择查询之前,这个 pre-query-flush 是为了确保数据库是一致的,否则你可能会结束选择您之前在事务中删除的实体。由于您在 PostUpdate 中执行选择,这将导致刷新,也许这就是 writeChanges() 被调用两次的原因。

让我感到奇怪的另一件事是您使用 PrePersist,但 PostUpdate - 为什么不对称,不应该是 PreUpdate?

编辑: 您可以尝试更改查询的刷新模式,将刷新延迟到提交 query.setFlushMode(),只要您不打算在同一事务中删除用户就可以了。

【讨论】:

只是为了补充您的答案:在更新语句进入数据库之后发生 PostUpdate。实体的更改不应同步到数据库,事件也不应根据 JPA 2.1 规范对实体进行关系更改:“一般而言,可移植应用程序的生命周期方法不应调用 EntityManager 或查询操作,访问其他实体实例,或修改同一持久性上下文中的关系[46]。[47] 生命周期回调方法可能会修改调用它的实体的非关系状态。” tyvm 为您解答 1)我正在尝试不同的回调方法。最初它是 PreUpdate。结果相同。 2)JPA FlushModeType 似乎只是一个提示Flushing to occur at transaction commit. The provider may flush at other times, but is not required to 3)根据@Chris 的评论,我的方法违反了JPA 规范,所以我应该寻求另一个解决方案,对吧?最后,我只想避免手动对每种服务方法进行审核。当然,这只是一个方法调用,但我认为有人忘记调用它的风险很高。有什么想法/替代方案吗?

以上是关于在 EntityListener 中使用带有 PersistenceContext 的 EJB 时,JPA 尝试在数据库中插入相同的实体两次的主要内容,如果未能解决你的问题,请参考以下文章

什么是pe系统

电脑pe是啥意思

浪潮服务器进pe按f几

如何在 EntityListeners 中注入 EntityManager

如何禁用警告“省略局部变量的类型注释”?在 Dart 中(带有 Pedantic 包)

PE文件格式分析