polish
This commit is contained in:
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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() {
|
||||
Reference in New Issue
Block a user