7

I'm using Hibernate Envers to persist object history. At certain points we want to capture a snapshot of the object graph state - and we can do this by knowing the corresponding Envers revision which we then store on an audit record.

However we have a problem. The parent object is updated within the same transaction where we create and store its child audit record - complete with Envers revision. We can get the latest revision:

Number revision = reader.getRevisionNumberForDate(new Date(Long.MAX_VALUE));

or create a new revision:

Number revision = reader.getCurrentRevision(DefaultRevisionEntity.class, true).getId();

and use either, but the commit of the parent always happens after that. And that is when Envers increments the revision. Hence the revision we actually need to reference in the audit record is always higher than the value stored. In the simplest case, we get and store revision N but the parent version we need is stored as N + 1.

The AuditReader reader reference can be obtained with:

JpaTransactionManager transactionManager;  // injected
EntityManagerFactory emf = transactionManager.getEntityManagerFactory();
EntityManager entityManager = emf.createEntityManager();
AuditReader reader = AuditReaderFactory.get(entityManager);

We're using Spring 3 @Transactional annotations and Hibernate 4.2.

Minimal class graph:

Parent.class
    int version           // for hibernate optimistic locking
    String revisionName
    List<AuditChild> audits

AuditChild.class
    int enversRevision    // use to retrieve previous graphs of parent

I've tried numerous approaches to force the commit of parent to occur first, among them:

  • Splitting the code across multiple methods with @Transactional(propagation = Propagation.REQUIRES_NEW)
  • Explicit invocation of entityManager.flush();

Everything I've tried has either had no effect or caused other problems. I'd be happy to hear of solutions which have worked for others. Thanks.

user598656
  • 442
  • 7
  • 10
  • The revision number should be unique to a transaction, are you sure that `reader.getCurrentRevision(DefaultRevisionEntity.class, true).getId()` doesn't work? You should need to reference the "current" revision for the current transaction. – adamw Apr 07 '14 at 04:56
  • @adamw Yes that creates a new revision. However when my enclosing transaction had been committed the actual revision on the relevant update rows in the *_AUD table(s) is > n (n + 1 when no other updates to the database overlap this tx). So the Id on the returned object from `getCurrentRevision()` wasn't the one applied when the current Tx was committed. My work-around is to create with a dummy (-1) revision and in a 2nd transaction to read back and update my revision reference. (Environment: Hibernate Envers 4.2, Spring 3.2 @Transactional annotated transactions; DB2 9.1). – user598656 Apr 08 '14 at 15:34
  • Hmm, well if you call getCurrentRevision() in a TX you should get the revision for that transaction. Do you have any flush()es in between? Though that shouldn't matter really. – adamw Apr 09 '14 at 05:21
  • @adamw Thanks for the responses. I realise I need to provide code for a complete reproduction case to take further. However my two Tx work-around is good enough for now. FYI I've commented on this issue also on https://community.jboss.org/thread/171696 – user598656 Apr 15 '14 at 08:04
  • Take a look at this blog post: http://azagorneanu.blogspot.com/2013/06/transaction-synchronization-callbacks.html , it may make your 2 TX workaround simpler by registering after-commit callback. – Krešimir Nesek Aug 25 '14 at 09:35

1 Answers1

1

An easier alternative would be to mak use of @Version.

First, Envers needs to be configured to actually audit the optimistic locking field's value, which it doesn't do by default. You do this by setting the following configuration:

org.hibernate.envers.do_not_audit_optimistic_locking_field=false

At this point, Envers will include the field in the audit history table and will replicate the assigned ORM version value to the audit history table. At this point, we have a unique way to join the ORM entity row with the audit history row using @Formula. The formula SQL would be:

SELECT e.REV
  FROM YourEntity_AUD e
 WHERE e.originalId.id = id  
   AND e.version = version   

Now its just as simple as adding a field to your entity:

@Formula( /* the SQL from above */)
@NotAudited
private Integer revisionNumber;

HTH.

Naros
  • 19,928
  • 3
  • 41
  • 71