JPA/Hibernate Temporary Conversations with Spring AOP

Today we discuss about temporary conversations in JPA, their usage, all usage warnings and a sample implementation using Spring AOP

This article follows upon a previous one about global long conversation : JPA/Hibernate Long session

I Abstract

A Definition

A temporary conversation is an unit of work from the end-user point of view whose lifetime is too short to deserve to be managed in the global conversation and whose concern is clearly distinct from the main business logic of the global conversation.

Wizard screens and windows popups are very good candidates to the temporary conversation pattern.


 

B Do’s & don’t

Do:

  • Clearly demarcate temporary conversation boundaries from the application flow. One single entry & exit point
  • One single extended EntityManager per conversation to manage all loaded entities
  • Commit/RollBack operations only at the end of the conversation

Don’t:

  • Mix many temporary conversations in one flow
  • Have many JDBC transactions in one temporary conversation
  • Abuse of temporary conversation

 

II Design

A Class Diagram

We re-use the same design as the one for global conversation. Though there are some differences:

  • the ConversationFilter is not used
  • we define a TemporaryConversationManager, extending the DefaultConversationManager and re-defining the beginConversation() & endConversation() methods
  • we define a TemporaryConversationAnnotationProcessor with additional methods : beginTemporaryConversation(), endTemporaryConversation() & interceptDaoExecution()

Please note that we also re-use the HttpSessionRepository as implementation for ConversationRepository although it is not shown in the above diagram.

 

II Algorithm

  • Before the execution of any method annotated by
    @BeginTemporaryConversation(conversationName=ConversationEnum.C1)

    • ConversationAnnotationProcessor.findEntityManagerFactoryFromContext(ConversationEnum.C1.emf())
    • Define token = ConversationEnum.C1.emf() + “-” + ConversationEnum.C1.name()
    • ConversationRepository.registerEntityManager(token,emf)
  •  

  • For each call to any method whose first argument is of type ConversationEnum
    • If the ConversationEnum parameter is null
      • Do nothing
    • Else
      • Save the Entity Manager attached to the Spring Thread Local having with search key = ConversationEnum.C1.emf() (TransactionSynchronizationManager)
    • ConversationAnnotationProcessor.findEntityManagerFactoryFromContext(ConversationEnum.C1.emf())
    • Define token = ConversationEnum.C1.emf() + “-” + ConversationEnum.C1.name()
    • ConversationManager.reattachEntityManager(token,emf)
    • Execute the method
    • ConversationManager.detachEntityManager(token)
    • Rebind the save Entity Manager to the Spring Thread Local
  •  

  • After the execution of any method annotated by
    @EndTemporaryConversation(conversationName=ConversationEnum.C1)

    • Define token = ConversationEnum.C1.emf() + “-” + ConversationEnum.C1.name()
    • ConversationRepository.unregisterEntityManager(token)

 

III Implementation

A ConversationEnum

We create a conversation enum to list all possible temporary conversations in the application. The advantages of this approach are:

  1. Have all the definition of temporary conversations at one place, refactoring will be easier
  2. A Java enum makes it easier to detect any typo in conversation naming
  3. Add extra properties to temporary conversation using enum structure
public enum ConversationEnum
{
	C1("myEntityManagerFactory"),
	C2("myEntityManagerFactory"),
	C3("myEntityManagerFactory"),
	C4("myAnotherEntityManagerFactory");

	public final String emf;

	ConversationEnum(String emf) {
		this.emf = emf;
	}
}

We define a public final property emf to indicates which EntityManagerFactory each temporary conversation is using. This information is mandatory for the annotation processor to look up and inject the right EntityManager for the conversation.

B @BeginTemporaryConversation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface BeginTemporaryConversation
{
	/**
	 * <p>
	 * Name of the current conversation requiring a temporary Entity Manager. 
	 * This name is mandatory since it is used as search key to register the temporary 
	 * Entity Manager to a ThreadLocal
	 * </p>
	 */
	ConversationEnum conversationName();

}

The conversationName is mandatory because we need a reference to the ConversationEnum object to get the associated Entity Manager Factory.

C @EndTemporaryConversation

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface EndTemporaryConversation
{
	/**
	 * <p>
	 * Name of the current conversation requiring a temporary Entity Manager. 
	 * This name is mandatory since it is used as search key to register the temporary 
	 * Entity Manager to a ThreadLocal
	 * </p>
	 */
	ConversationEnum conversationName();
}

D TemporaryConversationManager

public class TemporaryConversationManager extends DefaultConversationManager
{
	@Override
	public void startConversation(Object token, EntityManagerFactory emf)
	{
		EntityManager em = emf.createEntityManager();
		this.repository.registerEntityManager(token, em);
	}

	@Override
	public void endConversation(Object token, EntityManagerFactory emf)
	{
		EntityManager em = this.repository.findEntityManager(token);
		if (em != null && em.isOpen())
		{
			em.close();
		}
		this.repository.unregisterEntityManager(token);
	}
}

We re-define the startConversation() & endConversation() from the superclass to remove the call to reattachEntityManager() & detachEntityManager() respectively.

 

E TemporaryConversationAnnotationProcessor

@Aspect
public class TemporaryConversationAnnotationProcessor implements ConversationAnnotationProcessor,
Ordered, ApplicationContextAware
{

	private ConversationManager conversationManager;

	private ApplicationContext ctx;

	private int order;

	@Pointcut("execution(public * *(..)) && @annotation(beginTempConversationAnn)")
	public void temporaryConversationBegin(BeginTemporaryConversation beginTempConversationAnn)
	{}

	@Pointcut("execution(public * *(..)) && @annotation(endTemporaryConversationAnn)")
	public void temporaryConversationEnd(EndTemporaryConversation endTemporaryConversationAnn)
	{}

	@Pointcut("execution(* com.jpa.conversation*.dao..*()) && args(conversation,..)")
	public void daoExecutionJoinPoint(ConversationEnum conversation)
	{}

	@Before("temporaryConversationBegin(beginTempConversationAnn)")
	public void triggerBeginTemporaryConversation(BeginTemporaryConversation beginTempConversationAnn)
	{
		EntityManagerFactory emf = this.findEntityManagerFactoryFromContext(beginTempConversationAnn.conversationName().emf);
		String token = beginTempConversationAnn.conversationName().emf + "-" + beginTempConversationAnn.conversationName().name();
		this.conversationManager.startConversation(token, emf);
	}

	@After("temporaryConversationEnd(endTemporaryConversationAnn)")
	public void triggerEndTemporaryConversation(EndTemporaryConversation endTemporaryConversationAnn)
	{
		EntityManagerFactory emf = this.findEntityManagerFactoryFromContext(endTemporaryConversationAnn.conversationName().emf);
		String token = endTemporaryConversationAnn.conversationName().emf + "-" + endTemporaryConversationAnn.conversationName().name();
		this.conversationManager.endConversation(token, emf);
	}

	@Around("daoExecutionJoinPoint(conversation)")
	public Object interceptDaoExecution(ProceedingJoinPoint pjp, ConversationEnum conversation)
	{
		Object retValue = null;

		try
		{
			if (conversation == null)
			{
				retValue = pjp.proceed();
			}
			else
			{
				String token = conversation.emf + "-" + conversation.name();
				EntityManagerFactory emf = this.findEntityManagerFactoryFromContext(conversation.emf);
				EntityManagerHolder savedEmHolder = (EntityManagerHolder) TransactionSynchronizationManager.unbindResourceIfPossible(emf);

				this.conversationManager.reattachEntityManager(token, emf);

				retValue = pjp.proceed();

				this.conversationManager.detachEntityManager(emf);

				if (savedEmHolder != null)
				{
					TransactionSynchronizationManager.bindResource(emf, savedEmHolder);
				}

			}
		}
		catch (Throwable throwable)
		{

		}

		return retValue;
	}

	@Override
	public void triggerBeginConversation(BeginConversation annotation)
	{}

	@Override
	public void triggerEndConversation(EndConversation annotation)
	{}

	@Override
	public EntityManagerFactory findEntityManagerFactoryFromContext(String emf)
	{
		return (EntityManagerFactory) this.ctx.getBean(emf);
	}

	@Override
	public void setConversationManager(ConversationManager conversationManager)
	{
		this.conversationManager = conversationManager;

	}

	@Override
	public int getOrder()
	{
		return this.order;
	}

	public void setOrder(int order)
	{
		this.order = order;
	}

	@Override
	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
	{
		this.ctx = applicationContext;
	}
}

The implementation is quite straight-forward.

Please notice lines 20 & 21. We define the around pointcut for interceptDaoExecution() by restricting the method execution to interception to method in the DAO layer only. Indeed it is useless to intercept a method call at Service or view layer. The interception should be done only in the class where the Entity Manager injected by Spring’ @PersistenceConext is used

 

IV Usage

A Starting a new temporary conversation

@BeginTemporaryConversation(conversationName = ConversationEnum.C1)
public void doGetUserDetails(User user) {
	...
	UserDetails details = myDao.loadUserDetails(ConversationEnum.C1, user);
	...
}

In the above code, to start a new temporary conversation, we simply need to annotate it with the @BeginTemporaryConversation annotation, providing the conversation name and indicating whether the conversation should start a transaction or not.

Please notice in the example that we pass the ConversationEnum as the first parameter down to the DAO layer. This parameter is mandatory so the TemporaryConversationAnnotationProcessor knows that we’re currently in a temporary conversation context. The ConversationEnum is also necessary to get the name of the related Entity Manager Factory in Spring context and thus create the temporary Entity Manager.

B Accessing the temporary Entity Manager in DAO layer

public class MyDao {
	@PersistenceContext(unitName="myPersistenceUnit ")
	private  EntityManager em;

	public UserDetails loadUserDetails(ConversationEnum conversation, User user) {
		...
		em.createQuery(query);
		...
	} 
 }

The Entity Manager em in use in the method loadUserDetails() is injected by Spring. However, since the method loadUserDetails() is intercepted by the TemporaryConversationAnnotationProcessor, the injected Entity Manager is the one created for the temporary conversation.

C Ending a temporary conversation

@EndTemporaryConversation(conversationName = ConversationEnum.C1)
@Transactional(value = "myTransactionManager1", propagation = Propagation.REQUIRED)  
public void doUpdateUserDetails(User user) 
{  
	...
	// Clear references
	...
}

To end a conversation, we simply annotate the last method of the conversation with @EndTemporaryConversation.

Please notice the use of Spring standard @Transactional annotation to create a JDBC transaction. Conversations and JDBC transactions are both cross-cuting concerns and completely uncorrelated.

We should not forget to clean all references to entities that were loaded in the temporary Entity Manager and now become detached. These references should be removed in all beans, from Service to View layer if necessary. Forgetting to do so may result in the famous “Detached entities passed to persist/delete” Hibernate exception if these entities are re-used in another Entity Manager.

D Cancelling a temporary conversation

@TemporaryConversationEnd(conversationName = ConversationEnum.C1)
public void doCancelUserDetailsUpdate(User user) 
{  
	...
	// Clear references
	...
}

It is also possible to rollback a temporary conversation by removing Spring @Transactional annotation. DML statements will not be committed to the database outside of a transactional context.

2 Comments

  1. Pingback: JPA/Hibernate Conversational States Caveats « Yet Another Java Blog

  2. Pingback: JPA/Hibernate Global Conversation with Spring AOP « Yet Another Java Blog

Leave a Comment

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.