I want to achieve an auditing for some of my JPA entity classes via composition and some JPA callback methods. My current approach (shortened) looks like this:

Every entity which I want to audit implements the following simple interface:

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

Meta context is another JPA entity class which holds the audit information:

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

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

this is used via composition in my EntityClass

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

The AuditListener looks like this:

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());        
    }
}

Where SecurityService is another EJB which I am using to retrieve the User instance which belongs to the actual user who performs the action.

@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());
        }
    }
}

In each entity @TableGenerator and @GeneratedValue is used to create the IDs.


Behaviour: When calling EntityManager.persist on a fresh MyEntity instance, first the PrePersist method of my EntityListener is called, as expected. The date and the user is set correctly. After that the @PostUpdate method is called. this seems to be EclipseLink specific in combination with the generation of the ID. I noticed that before in other projects where I was using different approaches. However, now in the @PostUpdate method the service to find the current User is used again and this seems to cause that JPA wants to insert the MetaContext into the database several twice which of results in a violation of the Primaray Key constraint on this table. If I remove the PK constraint the operation finishes successfully but which the wrong result: whenUpdated is set to the current date, but whoUpdated is still null. In addition a Warning is logged which I do not really understand. The code above produces the following log:

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.

Now it gets even more strange: If I comment out the line // context.setWhoUpdated(securityService.getCurrentUser()); In the @PostUpdate method the logs indicate that the callback method is called (only) once, but whenUpdated remains null.

If there are questions or any information missing let me know and I will update the question.

If I do not use any update-callbacks at all, I can not spot any problem.

Soemone here who can explain the actual problem and / or knows how to fix my approach?

1

There are 1 answers

2
Klaus Groenbaek On

Disclosure: I have a pretty good understanding of JPA, but I have never used it in an EJB setting.

The identifier for the logged warning is nested_entity_manager_flush_not_executed_pre_query_changes_may_be_pending, and if you look at the code RepeatableWriteUnitOfWork (line 421, EL 2.5.2) you can see that a lot of code is skipped. For some reason the changes in the UnitOfWork is written twice.

One thing to remember is that, during an transaction EclipseLink flushes, (default flush-mode is auto) before it executes select queries, this pre-query-flush is to ensure that the database is consistent, otherwise you could end up selecting entities that you have previously deleted in the transaction. Since you perform a select inside PostUpdate, this will result in a flush, maybe this is why writeChanges() is invoked twice.

Another thing that strikes me as odd is that you use PrePersist, but PostUpdate - why the asymmetry, should it not be PreUpdate?

Edit: You could try to change the flush mode for your queries, to delay the flush until commit query.setFlushMode(), as long as you don't plan to delete the user in the same transaction you should be fine.