This commit is contained in:
Keith Donald
2007-08-01 20:33:30 +00:00
parent 37267b60f4
commit ef404cec12
3 changed files with 81 additions and 34 deletions

View File

@@ -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.
* <p>
* This implementation uses raw Hibernate APIs and binds the current session to the thread-local location identified by
* Spring's <code>HibernateTransactionManager</code>.
* The general pattern is as follows:
* <ul>
* <li>When a flow execution starts, create a new Hibernate Session and bind it to conversation scope.
* <li>Before processing a flow execution request, expose the conversationally-bound session as the "current session"
* for the current thread.
* <li>When an existing flow pauses, unbind the session from the current thread.
* <li>When an existing flow ends, commit the changes made to the session in a transaction if the ending state is a
* commit state. Then, unbind the context and close it.
* </ul>
*
* The general data access pattern implemented here is:
* <ul>
* <li> Create a new persistence context when a new flow execution with the 'persistenceContext' attribute starts
* <li> Load some objects into this persistence context
* <li> Perform edits to those objects over a series of conversational requests
* <li> On successful conversation completion, commit and flush those edits to the database, applying a version check if
* necessary.
* </ul>
*
* <p>
* 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.
* <p>
* 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
}
});

View File

@@ -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;
* <li>Before processing a flow execution request, expose the conversationally-bound persistence context as the
* "current" persistence context for the current thread.
* <li>When an existing flow pauses, unbind the persistence context from the current thread.
* <li>When an existing flow ends, unbind persistence context and and close it.
* <li>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.
* </ul>
*
* The general data access pattern implemented here is:
* <ul>
* <li> Create a new persistence context when a new conversation (e.g. edit session) starts
* <li> Load some objects non-transactionally using this persistence context
* <li> Create a new persistence context when a new flow execution with the 'persistenceContext' attribute starts
* <li> Load some objects into this persistence context
* <li> Perform edits to those objects over a series of conversational requests
* <li> On successful conversation completion, commit and flush those edits to the database
* <li> On successful conversation completion, commit and flush those edits to the database, applying a version check if
* necessary.
* </ul>
*
* 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.
* <p>
* 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.
* <p>
* 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);
}
}

View File

@@ -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() {