The previous 2 articles were dedicated to the analysis of Spring @Transactional and @PersistenceContext internals.
In this article we will discuss about the JPA extended persistence context (Hibernate long session) pattern. We’ll look at the pros and cosn and finally I will show a practical implementation using the TransactionSynchronizationManager class shown earlier in both articles
First, if you’ve missed the articles about Spring annotation, have a look here:
The following implementation is based on Spring 3.0.5 official release + JPA 2/Hibernate 3.5.2 in a WEB J2SE environment. If you are running applications on a J2EE platform, you should rely on transaction management facilities provided by the J2EE engine itself
Before going further, let’s define some notions:
- by session we mean Hibernate session (e.g. equivalent of entity manager)
- by user session we mean the user data context on the server. It includes all user data and session-scoped beans
- by persistence context, we mean a set of entities that are managed by the EntityManager. Sometimes people tend to use EntityManager when they mean persistence context.
- by transaction we mean JDBC transaction, not user transaction (unit-of-work or conversation). A transaction can be started by an EntityManager within a persistence context. Within the same persistence context, several transactions can exist, provided that their lifespan does not overlap since JPA does not support nested transactions
I The need for a long/extended session
The problem with the classical session-per-request or OSIV (Open Session In View) patterns is that for each new request made by the client browser to the server, a new EntityManager is created, attached to the current Thread using a ThreadLocal and is passed along all layers down to the DAOs. Once the response is sent back to the client browser, this EntityManager is closed so all entities that have been retrieved from the database are now in detached mode.
There are many drawbacks being in detached mode:
- the state of entities is not managed. Any modification to the field or collections on the entities is ignored by Hibernate while in detached mode. The next time they are re-attached to another EntityManager, all the modifications are lost
- the re-attachment of the entity itself is not that easy with the current state of JPA specs. JPA API does expose a detach() method on EntityManager but there is no re-attach()or something similar.
- one solution is to use merge() but merge() is not re-attaching your old detached entity, it copies all its state to a fresh entity created from scratch, synchronizing it with the database and returns the Java reference of the new entity. If your old detached entity is still referenced by other classes (service/DAO/presentation layers) you should update the reference with the new entity, which is quite cumbersome
- the other work-around is to use session.saveOrUpdate(detachedEntity) but it is worst because it will trigger sometimes UPDATE statement to the database since Hibernate does not know whether the state of this entity has changed or not while it was detached. Furthermore you need to do it on all detached entities so somehow you need to keep track (in a List) of all entities that may be detached to re-attach them automatically. Again this is very error-prone
The ultimate idea is to avoid detaching entities, keep them in the persistence context and let the EntityManager manage as long as the user unit-of-work lasts.
The difficulty now is where to store this opened EntityManager whose lifespan spreads across several client-server requests. Furthermore, we need to bind each living EntityManager to the correct user session. The natural placeholder is the HttpSession map.
II Pre-requisites
With global conversation, we need to:
- Clearly identify end-user conversation scenarios. An extended persistence context does not make sense if the developer cannot sketch out distinct web navigation scenarios and use cases.
- List all possible entry points for new conversations and their exit points to flush and close the EntityManager. Every conversation should have an exit point otherwise the EntityManager will never be closed and you’ll run into big trouble (OutOFMemoryException at least).
- Manage carefully the growth of the persistence context. Since the EntityManager lives across several user Web requests, the persistence context tends to grow bigger and bigger. You should load in the persistence context as few entities as possible, only those that are necessary for the current conversation.
- Usage of secondary level cache is strongly advised to discharge the EntityManager the burden of caching frequently used referential data.
- When you need to request data in the DB necessary for the logic of the user operations (functional code or label for instance), prefer scalar queries over object queries. Contrary to the latter, scalar results are not kept in the persistence context. If you run frequently some queries, consider using query cache.
- Consider creating additional temporary EntityManagers to load specifies entities, read their value and close the temporary EntityManager. This EntityManager is completely independent and isolated from the main EntityManager attached to the ThreadLocal in use. Look at Temporary Conversation with Spring AOP for more details.
- Consider detaching some entities when they are no longer used. For this, use the em.detach(entityToDetach) method. Beware of CascadeType configuration for collection associations in the entities candidate to be detached.
- Last but not least , find a convenient way to send the end-of-conversation signal to flush the persistence context and close the EntityManager. It is trickier than expected, especially when the signal is triggered at the Web tier and needs to be carried down to the DAO or servlet filter layer. Some available options : use another ThreadLocal to carry the signal (again!), use global variable, rely on AOP or carry a parameter along all method (brute force solution)
III Navigation models
As stated above, a clear navigation model for the GUI should be scketched out to help identifying entry/exit points for the global conversation. Below we introduce two types of navigation.
A Generic navigation
This is the most common type of navigation model. There are multiple entry and exit points. Some screens act as entry point for many scenario, some others act as exit point.
B Cyclic navigation
In this case, there is a single screen which is the pivot point for different navigation scenarios. Each new scenario starts at the same screen so the exit point for one conversation does match with the entry point for another.
III Design
A Main actors
This sample implementation consists of 4 actors:
- ConversationManager: the interface responsible for the management of the global conversation.
- ConversationRepository: repository interface to store the Entity Manager for the global conversation
- ConversationAnnotationProcessor: interface responsible for the interception of conversation demarcations
- AbstractConversationFilter: abstract class responsible for re-attaching/detaching the Entity Manager to the Thread Local between each request to the server
B Annotation
For the conversation demarcation, we introduce the following annotations:
- @BeginConversation(entityManagerFactory=”myEntityManagerFactory1″)
- @EndConversation(entityManagerFactory=”myEntityManagerFactory1″)
- @EndCyclicConversation(entityManagerFactory=”myEntityManagerFactory2″)
The first two annotations are used by global conversations. The last one is only usefull for cyclic conversations.
C ConversationManager
The ConversationManager interface exposes the following attributes & methods:
- repository: reference to a ConversationRepository instance
- beginConversation(Object token, EntityManagerFactory emf): start a new conversation using provided emf to create a global Entity Manager and the token to register it in the repository
- endConversation(Object token, EntityManagerFactory emf): end a conversation. Remove the registered Entity Manager from the repository using the token as search key
- reattachEntityManager(Object token, EntityManagerFactory emf): look uo the global Entity Manager in the repository using the token then attach it to the Thread Local (using Spring TransactionSynchronizationManager)
- detachEntityManager(EntityManagerFactory emf): detach the global Entity Manager from the Thread Local (using Spring TransactionSynchronizationManager)
- findEntityManager(Object token): helper method to look for the Entity Manager in the repository using the token
- hasConversation(Object token): helper method to check whether there is an on-going conversation with the token
D ConversationRepository
The ConversationRepository interface exposes 3 methods:
- registerEntityManager(Object token, EntityManager em): register the provided Entity Manager in the repository with token as search key
- unregisterEntityManager(Object token): remove the Entity Manager associated to the token from the repository
- findEntityManager(Object token): self-explanatory
There are several possible implementations for this interface. The default implementation is the HttpSessionRepository. It uses the current user HTTP session as storage and keeps an internal map of all Entity Managers.
E ConversationAnnotationProcessor
The ConversationAnnotationProcessor interfaces exposes the following attributes & methods:
- conversationManager: reference to the ConversationManager interface
- context: reference to the current Spring’ application context
- triggerBeginConversation(BeginConversation annotation): intercept any method annotated with @BeginConversation and start a new conversation
- triggerEndConversation(EndConversation annotation): intercept any method annotated with @EndConversation and terminate the conversation
- findEntityManagerFactoryInContext(String emf): find the Entity Manager Factory in the Spring context using its bean id
There are 2 possible implementations for this interface. The first one is the GlobalConversationAnnotationProcessor, suitable for global conversations. The second is the CyclicConversationAnnotationProcessor with an additional method: triggerEndCyclicConversation()
You may notice in the diagram the utility class ConversationUtils. This class simply helps to bind and unbind an Entity Manager to the Thread Local using Spring’ TransactionSynchronizationManager.
F ConversationFilter
The AbstractConversationFilter class exposes the following attributes and methods:
- conversationManager: reference to the Conversation Manager
- context: reference to the current Spring’ application context
- emf: Entity Manager Factory for the global conversation
- entityManagerFactoryBeanId: bean id of the Entity Manager Factory in the Spring’ context
- setEntityManagerFactoryBeanId(String beanId): setter for the entityManagerFactoryBeanId attribute
- destroy(): called when the filter is destroyed
Again, there are 2 implementations, GlobalConversationFilter & CyclicConversationFilter. The latter redefines the init() method to start a new conversation when the filter is initialized.
IV Algorithm
A Global conversation
- Before the execution of any method annotated by @BeginConversation(entityManagerFactory=”emf”)
- ConversationAnnotationProcessor.findEntityManagerFactoryFromContext(“emf”)
- ConversationRepository.registerEntityManager(“emf”,emf)
- ConversationManager.reattachEntityManager(“emf”,emf)
- For each call to the ConversationFilter
- If ConversationManager.hasConversation(“entityManagerFactoryBeanId”)
- ConversationManager.reattachEntityManager(“entityManagerFactoryBeanId”,emf)
- Continue the filter chain
- ConversationManager.detachEntityManager(emf)
- If ConversationManager.hasConversation(“entityManagerFactoryBeanId”)
- After the execution of any method annotated by @EndConversation(entityManagerFactory=”emf”)
- ConversationAnnotationProcessor.findEntityManagerFactoryFromContext(“emf”)
- ConversationManager.detachEntityManager(emf)
- Close the Entity Manager (em.close())
- ConversationRepository.unregisterEntityManager(“emf”)
B Cyclic conversation
- At the initialization (init()) of the ConversationFilter
- ConversationRepository.registerEntityManager(“emf”,emf)
- ConversationManager.reattachEntityManager(“emf”,emf)
- For each call to the ConversationFilter
- If ConversationManager.hasConversation(“entityManagerFactoryBeanId”)
- ConversationManager.reattachEntityManager(“entityManagerFactoryBeanId”,emf)
- Else
- Throw an IllegalStateException
- Continue the filter chain
- ConversationManager.detachEntityManager(emf)
- If ConversationManager.hasConversation(“entityManagerFactoryBeanId”)
- After the execution of any method annotated by @EndCyclicConversation(entityManagerFactory=”emf”)
- ConversationManager.detachEntityManager(emf)
- Close the Entity Manager (em.close())
- ConversationRepository.unregisterEntityManager(“emf”)
- ConversationRepository.registerEntityManager(“emf”,emf)
- ConversationManager.reattachEntityManager(“emf”,emf)
C Usage
1) Global conversation
public class Class1 { @BeginConversation(entityManagerFactory = "myEntityManagerFactory") public void myMethod() { ... } } public class DaoClass { @Transactional(value = "myTransactionManager", propagation = Propagation.REQUIRED) public void save() { ... } } public class Class2 { @EndConversation(entityManagerFactory = "myEntityManagerFactory") public void finalize() { ... } }
2) Cyclic conversation
public class Class1
{
@EndCyclicConversation(entityManagerFactory = "myEntityManagerFactory")
@Transactional(value = "myTransactionManager", propagation = Propagation.REQUIRED)
public void endCyclicConversationMethod()
{
...
}
}
V Implementation
A HttpSessionRepository
public class HttpSessionRepository implements ConversationRepository { private Map<Object, EntityManager> conversationMap = new HashMap<Object, EntityManager>(); @Override public EntityManager findEntityManager(Object token) { return this.conversationMap.get(token); } @Override public void registerEntityManager(Object token, EntityManager em) { this.conversationMap.put(token, em); } @Override public void unregisterEntityManager(Object token) { this.conversationMap.remove(token); } @Override public boolean containsEntityManager(Object token) { return this.conversationMap.containsKey(token); } }
Please notice that the token object is used to search for the Entity Manager in the internal Map. Consequently it should redefine the equals() & hashCode() methods.
B DefaultConversationManager
public class DefaultConversationManager implements ConversationManager { protected ConversationRepository repository; private static final Logger logger = LoggerFactory.getLogger(DefaultConversationManager.class); @Override public void startConversation(Object token, EntityManagerFactory emf) { EntityManager em = emf.createEntityManager(); this.repository.registerEntityManager(token, em); this.reattachEntityManager(token, emf); } @Override public void endConversation(Object token, EntityManagerFactory emf) { this.detachEntityManager(emf); EntityManager em = this.repository.findEntityManager(token); if (em != null && em.isOpen()) { em.close(); } this.repository.unregisterEntityManager(token); } @Override public void reattachEntityManager(Object token, EntityManagerFactory emf) { if (this.repository.containsEntityManager(token)) { EntityManager em = this.repository.findEntityManager(token); if (!TransactionSynchronizationManager.hasResource(emf)) { EntityManagerHolder newEmHolder = new EntityManagerHolder(em); ConversationUtils.registerResources(emf, newEmHolder); logger.debug("Reattach the Entity Manager " + em.hashCode() + " to the current Thread Local"); } else { throw new IllegalStateException("There is an Entity Manager already bound to the Thread Local with the Entity Manager Factory " + emf.hashCode()); } } else { throw new IllegalStateException("There is no Entity Manager in the Conversation Repository with the token " + token); } } @Override public void detachEntityManager(EntityManagerFactory emf) { if (TransactionSynchronizationManager.hasResource(emf)) { ConversationUtils.unregisterPreviousResources(emf); logger.debug("Detach the Entity Manager from the current Thread Local"); } } @Override public EntityManager findEntityManager(Object token) { return this.repository.findEntityManager(token); } @Override public boolean hasConversation(Object token) { return this.repository.containsEntityManager(token); } @Override public void setConversationRepository(ConversationRepository repository) { this.repository = repository; } }
The implementation is quite straight-forward.
C Annotations
1) @BeginConversation
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface BeginConversation { /** * <p> * Mandatory parameter. Name of the entityManagerFactory bean defined in * the Spring XML configuration * Necessary to create new Entity Manager * </p> */ String entityManagerFactory(); }
2) @EndConversation
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface EndConversation { /** * <p> * Mandatory parameter. Name of the entityManagerFactory bean defined in * the Spring XML configuration * Necessary to create new Entity Manager * </p> */ String entityManagerFactory(); }
3) @EndCyclicConversation
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @Documented public @interface EndCyclicConversation { /** * <p> * Mandatory parameter. Name of the entityManagerFactory bean defined in * the Spring XML configuration * Necessary to create new Entity Manager * </p> */ String entityManagerFactory(); }
D GlobalConversationAnnotationProcessor
@Aspect public class GlobalConversationAnnotationProcessor implements ConversationAnnotationProcessor, Ordered, ApplicationContextAware { private ConversationManager conversationManager; private ApplicationContext ctx; private int order; @Pointcut("execution(public * *(..)) && @annotation(beginConversationAnn)") public void conversationBegin(BeginConversation beginConversationAnn) {} @Pointcut("execution(public * *(..)) && @annotation(endConversationAnn)") public void conversationEnd(EndConversation endConversationAnn) {} @Before("conversationBegin(beginConversationAnn)") public void triggerBeginConversation(BeginConversation beginConversationAnn) { EntityManagerFactory emf = this.findEntityManagerFactoryFromContext(beginConversationAnn.entityManagerFactory()); this.conversationManager.startConversation(beginConversationAnn.entityManagerFactory(), emf); } @After("conversationEnd(endConversationAnn)") public void triggerEndConversation(EndConversation endConversationAnn) { EntityManagerFactory emf = this.findEntityManagerFactoryFromContext(endConversationAnn.entityManagerFactory()); this.conversationManager.endConversation(endConversationAnn.entityManagerFactory(), emf); } @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 GlobalConversationAnnotationProcessor is a Spring AOP aspect. It defines 2 pointcuts to intercept any method annotated with @BeginConversation or @EndConversation then delegates the processing to the Conversation Manager.
Please notice the Ordered interface implemented by this class. This is important to define the order of this aspect when many aspects are applied to the same method.
More reading about advice ordering here: Spring advice ordering
Ideally this advice should be applied first and wrap all other advices.
A simple use case of this advice ordering is when @EndConversation is used in conjunction with Spring @Transactional. The @EndConversation advice should be the first advice to kick in and the last to kick out to flush the session and close the EntityManager.
E CyclicConversationAnnotationProcessor
@Aspect public class CyclicConversationAnnotationProcessor implements ConversationAnnotationProcessor, Ordered, ApplicationContextAware { private ConversationManager conversationManager; private ApplicationContext ctx; private int order; @Pointcut("execution(public * *(..)) && @annotation(endCyclicConversationAnn)") public void cyclicConversationEnd(EndCyclicConversation endCyclicConversationAnn) {} @Override public void triggerBeginConversation(BeginConversation annotation) {} @Override public void triggerEndConversation(EndConversation annotation) {} @After("cyclicConversationEnd(endCyclicConversationAnn)") public void triggerEndCyclicConversation(EndCyclicConversation endCyclicConversationAnn) { EntityManagerFactory emf = this.findEntityManagerFactoryFromContext(endCyclicConversationAnn.entityManagerFactory()); this.conversationManager.endConversation(endCyclicConversationAnn.entityManagerFactory(), emf); this.conversationManager.startConversation(endCyclicConversationAnn.entityManagerFactory(), emf); } @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; } }
This implementation introduces a new method: triggerEndCyclicConversation(). What is does it simply call ConversationManager.startConversation() as soon as the previous conversation is finished (lines 27 & 28)
F AbstractConversationFilter
public abstract class AbstractConversationFilter implements Filter, ApplicationContextAware, InitializingBean { protected ConversationManager conversationManager; protected ApplicationContext ctx; protected EntityManagerFactory emf; protected String entityManagerFactoryBeanId; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.ctx = applicationContext; } public void setEntityManagerFactoryBeanId(String emfBeanId) { this.entityManagerFactoryBeanId = emfBeanId; } public void setConversationManager(ConversationManager conversationManager) { this.conversationManager = conversationManager; } @Override public void destroy() { this.conversationManager.detachEntityManager(this.emf); this.conversationManager.endConversation(this.entityManagerFactoryBeanId, this.emf); } @Override public void afterPropertiesSet() throws Exception { if (StringUtils.isBlank(this.entityManagerFactoryBeanId)) { throw new ServletException("The property 'entityManagerFactoryBeanId' has not been set"); } else { this.emf = (EntityManagerFactory) this.ctx.getBean(this.entityManagerFactoryBeanId); if (this.emf == null) { throw new IllegalStateException("The Entity Manager Factory '" + this.entityManagerFactoryBeanId + "' cannot be found in the Spring context"); } } }
The AbstractConversationFilter class provides a default implementation for the destroy() method. It also takes care of the initialization process through the afterPropertiesSet() method of InitializingBean interface.
If the entityManagerFactoryBeanId property is empty or if the corresponding Entity Manager Factory cannot be found in the Spring context, the filter raises an exception. It offers a convenient fail-fast behavior.
G GlobalConversationFilter
private static final Logger logger = LoggerFactory.getLogger(GlobalConversationFilter.class); @Override public void init(FilterConfig arg0) throws ServletException {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filerChain) throws IOException, ServletException { if (this.conversationManager.hasConversation(this.entityManagerFactoryBeanId)) { logger.debug("Continuing conversation"); this.conversationManager.reattachEntityManager(this.entityManagerFactoryBeanId, this.emf); } try { filerChain.doFilter(request, response); } catch (Exception ex) { // Exception handling } finally { this.conversationManager.detachEntityManager(this.emf); } }
The GlobalConversationFilter implementation is quite straight-forward. The init() method is left empty since initial checks are already done in afterPropertiesSet() of the superclass.
H CyclicConversationFilter
private static final Logger logger = LoggerFactory.getLogger(CyclicConversationFilter.class); @Override public void init(FilterConfig arg0) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filerChain) throws IOException, ServletException { boolean newConversation = this.initNewConversationIfNecessary(); if (this.conversationManager.hasConversation(this.entityManagerFactoryBeanId) && !newConversation) { logger.info("Continuing conversation"); this.conversationManager.reattachEntityManager(this.entityManagerFactoryBeanId, this.emf); } try { filerChain.doFilter(request, response); } catch (Exception ex) { // Exception handling } finally { this.conversationManager.detachEntityManager(this.emf); } } private boolean initNewConversationIfNecessary() { boolean newConversation = false; if (!this.conversationManager.hasConversation(this.entityManagerFactoryBeanId)) { logger.info("Starting new conversation"); this.conversationManager.startConversation(this.entityManagerFactoryBeanId, this.emf); newConversation = true; } return newConversation; }
The CyclicConversationFilter does redifines the init() method to start automatically a new conversation. Furthermore, an exception is raised if the filter cannot find an on-going conversation in the repository at each server request. Indeed with a cyclic conversation we are supposed to always have an on-going conversation.
I Spring configuration
<bean id="conversationRepository" class="com.jpa.conversation.repository.HttpSessionRepository" scope="session"> <aop:scoped-proxy/> </bean> <bean id="conversationManager" class="com.jpa.conversation.manager.DefaultConversationManager"> <property name="conversationRepository" ref="conversationRepository"/> </bean> <!-- Global Conversation --> <bean id="conversationAnnotationProcessor" class="com.jpa.conversation.annotation.processor.GlobalConversationAnnotationProcessor"> <property name="conversationManager" ref="conversationManager"/> <property name="order" value="1"/> </bean> <bean id="conversationFilter" class="com.jpa.conversation.filter.GlobalConversationFilter"> <property name="entityManagerFactoryBeanId" value="myEntityManagerFactory"/> <property name="conversationManager" ref="conversationManager"/> </bean> <!-- Cyclic Conversation --> <bean id="conversationAnnotationProcessor" class="com.jpa.conversation.annotation.processor.CyclicConversationAnnotationProcessor"> <property name="conversationManager" ref="conversationManager"/> <property name="order" value="1"/> </bean> <bean id="conversationFilter" class="com.jpa.conversation.filter.CyclicConversationFilter"> <property name="entityManagerFactoryBeanId" value="myEntityManagerFactory"/> <property name="conversationManager" ref="conversationManager"/> </bean>
Please notice the
Normally it does not make sense to inject a session bean into a singleton bean. The
So by defining the Conversation Repository with session scope, it is indeed unique for each user session.
J web.xml configuration
<filter> <filter-name>globalConversationFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>conversationFilter</param-value> </init-param> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> ... <filter> <filter-name>requestContextFilter</filter-name> <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class> </filter> ... <filter-mapping> <filter-name>requestContextFilter</filter-name> <url-pattern>/pages/myModule/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>globalConversationFilter</filter-name> <url-pattern>/pages/myModule/*</url-pattern> </filter-mapping> ...
Instead of declaring the Conversation Filter directly in the web.xml file we rely on Spring DelegatingFilterProxy filter. This filter just delegates the processing to a bean in Spring context, thus allowing dependency injection.
Pay attention to lines 5 & 6 where we indicates the id of the target bean in Spring context. It is possible to omit this information, in this case Spring will look for the bean having the id equals to the filter name (globalConversationFilter).
At lines 9 & 10, we set targetFilterLifecycle to true to tells Spring to propagates the servlet lifecycle to the target bean. According to the documentation, the init() & destroyed() methods at the target bean are not invoked unless we activate this option.
EntityManagerFactory4Ever
Pingback: JPA/Hibernate temporary conversations with Spring AOP « Yet Another Java Blog
Pingback: JPA/Hibernate Conversational States Caveats « Yet Another Java Blog
This is a good blog post.
However here’s a few remarks :
– If possible I would avoid all coupling with HTTP stuff, if you leave in a Spring container you can have ConversationRepository or something like that.
– The sample code is hardely testable, you should extract responsibilities in smaller objects. For example instead of the whole if/else smala you consider the use of strategy pattern that could be set in the annotation as a class or an enum. (Good design is always SOLID and based around messaging between collaborators. OOP is mostly about messaging.)
– If identifying conversations entry and exit point is important I would strongly consider annotations that mark the beginning of a conversation, maybe also the continuation of a conversation.
– The conversation lifecycle annotations and the ConversationRepository could then allow timeouts on the conversation.
Thank you for the valuable remarks Brice.
– for the coupling with HTTP stuff, the idea is to have one global conversation per user session. The HTTP Session object is the easiest (and yet laziest) choice. However, for a more robust design, a ConversationRepository may be required as you said. In this case, we need to introduce a token system like “1 token = 1 unique user session Id”
– for testability, I agree that the sample code looks like spaghetti. I’ll refactor it a little bit. The original idea of the implementation is not to create dozen of classes and to concentrate the core functionalities in few locations.
– for the beginning of conversation demarcation, I took the approach to have conversation cycles. End of one conversation will automatically start a new one after JDBC commit. It’s a pure design choice for my sample application. Indeed I should also provide sample code for the beginning of conversation management
– timeout is clearly an extra feature 🙂 . I’ll implement it later
No adherence to HTTP means you can use any identifier for the conversation, be it a session id coming from the HTTP integration layer, or a custom id, maybe a business conversation id that can be generated through the ConversationRepository.
The strategy stuff might have other benefits, you can encapsulate EntityManager management in related strategies. So if you actually want to change the way your conversations are managed you can refactor it more easily.
For example I worked on a SOA application, we had conversations based on sticky sessions. If a machine went down we lost all the sessions and broke the user interactions, this is really bad for the client impacted by such incidents. So we changed our strategy to stateless services, with a centralized conversation repository. As we were stateless we didn’t need any HTTP session, so we could guarantee all started conversations could be ended normally on another machine.
Anyway this more relevant in a technical framwork.
Hello Brice
Big code refactoring this weekend. Now the code is quite clean.
I split the jobs between several classes. The architecture is now flexible and easier to test.
The post about Temporary Conversation will also be cleaned up.
By the way I also updated the call for paper slides with a newer version
Hi,
Oh yeah I wasn’t commenting with the CFP in mind, but more as a peer architecture review 😉
The code is greatly shaping up! Though I think the code still has too much coupling with the entity manager; imagine architecture that do not use an entity manager. That’s why I had strategies in mind (see the previous comment), however I confer the topic of this post is related to conversations with JPA / Hibernate.
Also about the TTL of the conversation, instead of using another threadlocal directly, it might be interesting to see what Java cache libraries have to offer with timeouts and removal listener.
Anyway thanx for sharing this code with us.
Hi,
It’s the most complete solution I’ve seen for long conversation session support with Spring, thanks.
Is it possible to download a sample project for this article so that I can try it by myself?
Best wishes,
Hello Zhou
Currently there is no sample project for this design. I should create one in few time to illustrate the concept and put it on GitHub
Thanks. Your Spring Series are the best Spring internal articles I’ve ever seen. It saves me a lot of time and make me confident with Spring “magic”
Is there any chance you can put the code on GIHub?