From ef404cec1280c9b172ddd0c478b88f2703ce6e37 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Wed, 1 Aug 2007 20:33:30 +0000 Subject: [PATCH] polish --- ...va => HibernateFlowExecutionListener.java} | 47 ++++++++++----- ...ner.java => JpaFlowExecutionListener.java} | 60 ++++++++++++++----- ... HibernateFlowExecutionListenerTests.java} | 8 +-- 3 files changed, 81 insertions(+), 34 deletions(-) rename spring-webflow/src/main/java/org/springframework/webflow/support/persistence/{HibernateSessionPerConversationListener.java => HibernateFlowExecutionListener.java} (65%) rename spring-webflow/src/main/java/org/springframework/webflow/support/persistence/{JpaSessionPerConversationListener.java => JpaFlowExecutionListener.java} (54%) rename spring-webflow/src/test/java/org/springframework/webflow/support/persistence/{HibernateSessionPerConversationListenerTests.java => HibernateFlowExecutionListenerTests.java} (97%) diff --git a/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateFlowExecutionListener.java similarity index 65% rename from spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java rename to spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateFlowExecutionListener.java index e2b20cbd..c25a0df1 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/HibernateFlowExecutionListener.java @@ -21,7 +21,6 @@ import org.hibernate.SessionFactory; import org.springframework.orm.hibernate3.SessionHolder; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; @@ -34,23 +33,44 @@ import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.ViewSelection; /** - * A {@link FlowExecutionListener} that implements the Hibernate Session-per-Conversation pattern as described in Java - * Persistence with Hibernate (chapter 11). + * A {@link FlowExecutionListener} that implements the Session-per-Conversation pattern using the native Hibernate API. *

- * This implementation uses raw Hibernate APIs and binds the current session to the thread-local location identified by - * Spring's HibernateTransactionManager. + * The general pattern is as follows: + *

+ * + * The general data access pattern implemented here is: + * + * *

- * This listener assumes that you are accessing Hibernate via Spring support such as HibernateTemplate or the - * LocalSessionFactoryBean. If not, Hibernate data access code will not participate in the proper transaction. + * Note: All data access except for the final commit will, by default, be non-transactional. However, a flow may call + * into a transactional service layer to fetch objects during the conversation in the context of a read-only system + * transaction. In that case, the session's flush mode will be set to Manual and no intermediate changes will be + * flushed. *

- * Note that when accessing service layer methods with Spring managed transactions, those transaction should have - * {@link Propagation#REQUIRED} semantics. Anything else defeats the purpose of holding the transaction open until the - * end of the session. + * Care should be taken to prevent premature commits of conversational data while the conversation is in progress. You + * would generally not want intermediate flushing to happen, as the nature of a conversation implies a transient, + * isolated resource that can be canceled before it ends. Generally, the only time a read-write transaction should be + * started is upon successful completion of the conversation, triggered by reaching a 'commit' end state. * * @author Ben Hale + * @author Keith Donald + * @author Juergen Hoeller * @since 1.1 */ -public class HibernateSessionPerConversationListener extends FlowExecutionListenerAdapter { +public class HibernateFlowExecutionListener extends FlowExecutionListenerAdapter { private static final String HIBERNATE_SESSION_ATTRIBUTE = "hibernate.session"; @@ -62,8 +82,7 @@ public class HibernateSessionPerConversationListener extends FlowExecutionListen * Create a new Session-per-Conversation listener using giving Hibernate session factory. * @param sessionFactory the session factory to use */ - public HibernateSessionPerConversationListener(SessionFactory sessionFactory, - PlatformTransactionManager transactionManager) { + public HibernateFlowExecutionListener(SessionFactory sessionFactory, PlatformTransactionManager transactionManager) { this.sessionFactory = sessionFactory; this.transactionTemplate = new TransactionTemplate(transactionManager); } @@ -94,7 +113,7 @@ public class HibernateSessionPerConversationListener extends FlowExecutionListen transactionTemplate.execute(new TransactionCallbackWithoutResult() { protected void doInTransactionWithoutResult(TransactionStatus status) { sessionFactory.getCurrentSession(); - // nothing to do - a flush will happen on commit automatically as this is a read-write + // nothing to do; a flush will happen on commit automatically as this is a read-write // transaction } }); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaSessionPerConversationListener.java b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaFlowExecutionListener.java similarity index 54% rename from spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaSessionPerConversationListener.java rename to spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaFlowExecutionListener.java index 99f6fd45..b537f4fd 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaSessionPerConversationListener.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/support/persistence/JpaFlowExecutionListener.java @@ -19,7 +19,11 @@ import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; import org.springframework.webflow.core.collection.AttributeMap; import org.springframework.webflow.execution.FlowExecutionException; import org.springframework.webflow.execution.FlowExecutionListener; @@ -38,43 +42,56 @@ import org.springframework.webflow.execution.ViewSelection; *

  • Before processing a flow execution request, expose the conversationally-bound persistence context as the * "current" persistence context for the current thread. *
  • When an existing flow pauses, unbind the persistence context from the current thread. - *
  • When an existing flow ends, unbind persistence context and and close it. + *
  • When an existing flow ends, commit the changes made to the persistence context in a transaction if the ending + * state is a commit state. Then, unbind the context and close it. * * * The general data access pattern implemented here is: * * - * Note: care should be taken to ensure at the service-layer that all data access in a conversation occurs - * non-transactionally until the final "commit" request to ensure isolation of intermediate object changes made during - * the course of the conversation. This care should be taken because, by default, JPA will always flush upon transaction - * commit, resulting in changes in the object model being synchronized with the database at that time. You would - * generally not want such intermediate flushing to happen, as the nature of a conversation implies a transient resource - * that can be canceled. + *

    + * Note: All data access except for the final commit will, by default, be non-transactional. However, a flow may call + * into a transactional service layer to fetch objects during the conversation in the context of a read-only system + * transaction if the underlying JPA Transaction Manager supports this. Spring's JPA TransactionManager does support + * this when working with a Hibernate JPA provider, for example. In that case, Spring will handle setting the FlushMode + * to MANUAL to ensure any in-progress changes to managed persistent entities are not flushed, while reads of new + * objects occur transactionally. + *

    + * Care should be taken to prevent premature commits of conversational data while the conversation is in progress. You + * would generally not want intermediate flushing to happen, as the nature of a conversation implies a transient, + * isolated resource that can be canceled before it ends. Generally, the only time a read-write transaction should be + * started is upon successful completion of the conversation, triggered by reaching a 'commit' end state. * * @author Keith Donald + * @author Juergen Hoeller * @since 1.1 */ -public class JpaSessionPerConversationListener extends FlowExecutionListenerAdapter { +public class JpaFlowExecutionListener extends FlowExecutionListenerAdapter { private static final String ENTITY_MANAGER_ATTRIBUTE = "jpa.entityManager"; private EntityManagerFactory entityManagerFactory; + private TransactionTemplate transactionTemplate; + /** * Create a new Session-per-Conversation listener using given JPA Entity Manager factory. * @param entityManagerFactory the entity manager factory to use */ - public JpaSessionPerConversationListener(EntityManagerFactory entityManagerFactory) { + public JpaFlowExecutionListener(EntityManagerFactory entityManagerFactory, + PlatformTransactionManager transactionManager) { this.entityManagerFactory = entityManagerFactory; + this.transactionTemplate = new TransactionTemplate(transactionManager); } public void sessionCreated(RequestContext context, FlowSession session) { - if (session.isRoot()) { + if (session.isRoot() && session.getDefinition().getAttributes().contains("persistenceContext")) { EntityManager em = entityManagerFactory.createEntityManager(); context.getConversationScope().put(ENTITY_MANAGER_ATTRIBUTE, em); bind(em); @@ -91,7 +108,18 @@ public class JpaSessionPerConversationListener extends FlowExecutionListenerAdap public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { if (session.isRoot()) { - EntityManager em = (EntityManager) context.getConversationScope().remove(ENTITY_MANAGER_ATTRIBUTE); + final EntityManager em = (EntityManager) context.getConversationScope().remove(ENTITY_MANAGER_ATTRIBUTE); + Boolean commitStatus = session.getState().getAttributes().getBoolean("commit"); + if (Boolean.TRUE.equals(commitStatus)) { + // this is a commit end state - start a new transaction that quickly commits + transactionTemplate.execute(new TransactionCallbackWithoutResult() { + protected void doInTransactionWithoutResult(TransactionStatus status) { + em.joinTransaction(); + // nothing to do - a flush will happen on commit automatically as this is a read-write + // transaction + } + }); + } unbind(em); em.close(); } @@ -108,10 +136,10 @@ public class JpaSessionPerConversationListener extends FlowExecutionListenerAdap } private void bind(EntityManager em) { - TransactionSynchronizationManager.bindResource(em, new EntityManagerHolder(em)); + TransactionSynchronizationManager.bindResource(entityManagerFactory, new EntityManagerHolder(em)); } private void unbind(EntityManager em) { - TransactionSynchronizationManager.unbindResource(em); + TransactionSynchronizationManager.unbindResource(entityManagerFactory); } } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListenerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateFlowExecutionListenerTests.java similarity index 97% rename from spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListenerTests.java rename to spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateFlowExecutionListenerTests.java index f142f88f..a74cdb95 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateSessionPerConversationListenerTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/support/persistence/HibernateFlowExecutionListenerTests.java @@ -41,11 +41,11 @@ import org.springframework.webflow.test.MockFlowSession; import org.springframework.webflow.test.MockRequestContext; /** - * Tests for {@link HibernateSessionPerConversationListener} + * Tests for {@link HibernateFlowExecutionListener} * * @author Ben Hale */ -public class HibernateSessionPerConversationListenerTests extends TestCase { +public class HibernateFlowExecutionListenerTests extends TestCase { private SessionFactory sessionFactory; @@ -53,7 +53,7 @@ public class HibernateSessionPerConversationListenerTests extends TestCase { private HibernateTemplate hibernateTemplate; - private HibernateSessionPerConversationListener listener; + private HibernateFlowExecutionListener listener; protected void setUp() throws Exception { DataSource dataSource = getDataSource(); @@ -63,7 +63,7 @@ public class HibernateSessionPerConversationListenerTests extends TestCase { hibernateTemplate = new HibernateTemplate(sessionFactory); hibernateTemplate.setCheckWriteOperations(false); HibernateTransactionManager tm = new HibernateTransactionManager(sessionFactory); - listener = new HibernateSessionPerConversationListener(sessionFactory, tm); + listener = new HibernateFlowExecutionListener(sessionFactory, tm); } public void testSameSession() {