当在EntityListener

时间:2017-01-03 20:24:44

标签: jpa eclipselink java-ee-7 jpa-2.1 entitylisteners

我想通过组合和一些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想要插入MetaContext进入数据库几次,导致违反此表上的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.

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

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

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

Soemone在这里谁可以解释实际问题和/或知道如何解决我的方法?

1 个答案:

答案 0 :(得分:3)

披露:我对JPA非常了解,但我从未在EJB设置中使用它。

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

要记住的一件事是,在执行select查询之前EclipseLink刷新(默认刷新模式为auto)的事务中,此pre-query-flush是为了确保数据库是一致的,否则你最终可能会选择您之前在交易中删除的实体。由于你在PostUpdate中执行select,这将导致刷新,这可能就是为什么writeChanges()被调用了两次。

令我感到奇怪的另一件事是你使用坚持,但发布更新 - 为什么不对称,如果它不是 PreUpdate

修改 您可以尝试更改查询的刷新模式,延迟刷新直到提交query.setFlushMode(),只要您不打算在同一事务中删除用户就应该没问题。