Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Mixing declarative and programmatic transactions with Spring and JPA listeners

I'm using a JPA EntityListener to do some additional audit work and am injecting a Spring-managed AuditService into my AuditEntryListener using @Configurable. The AuditService generates a collection of AuditEntry objects. The AuditService is itself a Singleton scoped bean, and I'd like to gather all the AuditEntry objects under a common key that can then be accessed by the outermost service layer (the one that invoked the persist call which in turn triggered the EntityListener).

I'm looking at using Spring's TransactionSynchronizationManager to set a specific transaction name (using UID() or some other unique strategy) at the beginning of the transaction, and then using that name as a key within the AuditService that will allow me to group all AuditEntry objects created within that transaction.

Is mixing declarative and programmatic transaction management have the potential for trouble? (Though I'm doing nothing more than setting the transaction name). Is there a better way to associate the generated AuditEntry objects with the current transaction? This solution does work for me, but given that the TransactionSynchronizationManager isn't intended for application use, I'd like to make sure that my use of it won't cause some unforseen problems.

Related Question

Finally, a related, but not immediately pertinent question: I know that the documentation for JPA EntityListeners cautions against using the current EntityManager, but if I did want to use it to diff an object against it's persisted self, would I be safe using an @Transactional(propagation=REQUIRES_NEW) annotation around my preUpdate() method?

Prototype Code:

Service Class

@Transactional
public void create(MyEntity e) {

    TransactionSynchronizationManager.setCurrentTransactionName(new UID().toString());
    this.em.persist(e);
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
        @Override
        public void afterCommit() {
            Set<AuditEntry> entries = auditService.getAuditEntries(TransactionSynchronizationManager.getCurrentTransactionName());
            if(entries != null) {
                for(AuditEntry entry : entries) {
                   //do some stuff....
                   LOG.info(entry.toString());
                }
            }
        }
    });
}

JPA EntityListener

@Configurable
public class AuditEntryListener {

@Autowired
private AuditService service;

@PreUpdate
public void preUpdate(Object entity) {
    service.auditUpdate(TransactionSynchronizationManager.getCurrentTransactionName(), entity);
}

public void setService(AuditService service) {
    this.service = service;
}

public AuditService getService() {
    return service;
}

}

AuditService

@Service
public class AuditService {
private Map<String, Set<AuditEntry>> auditEntryMap = new HashMap<String, Set<AuditEntry>>();

public void auditUpdate(String key, Object entity) {
    // do some audit work
    // add audit entries to map
    this.auditEntryMap.get(key).add(ae);
}

}
like image 644
Fil Avatar asked Jun 14 '11 15:06

Fil


1 Answers

@Filip

As far as I understand, your requirement is:

  1. Have an unique token generated within each transaction (database transaction of course)
  2. Keep this unique token easily accessible across all layers

So naturally you're thinking about the TransactionSynchronizationManager provided by Spring as a facility to store the unique token (in this case, an UID)

Be very carefull with this approach, the TransactionSynchronizationManager is the main storage helper to manage all the @Transactional processing for Spring. Under the @Transactional hood, Spring is creating an appropriate EntityManager, an appropriate Synchronization object and attach them to a thread local using TransactionSynchronizationManager.

In your service class code, inside a @Transactional method your are tampering with the Synchronization object, it can end up with undesirable behavior.

I've done an indept analysis of how @Transactional works here, have a look: http://doanduyhai.wordpress.com/2011/11/20/spring-transactional-explained/

Now back to your needs. What you can do is:

  1. Add a Thread local to the AuditService, containing the unique token when entering the @Transactional method and destroy it when exiting the method. Within this method call, you can access the unique token in any layer. Explanation for ThreadLocal usage can be found here: http://doanduyhai.wordpress.com/2011/12/04/threadlocal-explained/
  2. Create a new annotation, let's say @Auditable(uid="AuditScenario1") to annotate methods that need to be audited and use Spring AOP to intercept these method calls and manage the Thread local processing for you

    Example:

Modified AuditService

@Service
public class AuditService {

public uidThreadLocal = new ThreadLocal<String>();
...
...
}

Auditable annotation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface Auditable 
{
    String uid();
}

Usage of @Auditable annotation

@Auditable(uid="AuditScenario1")
@Transactional
public void myMethod()
{
   // Something 
}

Spring AOP part

@Around("execution(public * *(..)) && @annotation(auditableAnnotation)) 
public Object manageAuditToken(ProceedingJoinPoint jp, Auditable auditableAnnotation)
{
    ...
    ...
    AuditService.uidThreadLocal.set(auditableAnnotation.uid())...
    ...
}

Hope this will help.

like image 183
doanduyhai Avatar answered Sep 19 '22 18:09

doanduyhai