View Javadoc
1   /**
2    *    Copyright 2010-2015 the original author or authors.
3    *
4    *    Licensed under the Apache License, Version 2.0 (the "License");
5    *    you may not use this file except in compliance with the License.
6    *    You may obtain a copy of the License at
7    *
8    *       http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *    Unless required by applicable law or agreed to in writing, software
11   *    distributed under the License is distributed on an "AS IS" BASIS,
12   *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *    See the License for the specific language governing permissions and
14   *    limitations under the License.
15   */
16  package org.mybatis.spring;
17  
18  import static org.springframework.util.Assert.notNull;
19  
20  import org.apache.ibatis.exceptions.PersistenceException;
21  import org.apache.ibatis.logging.Log;
22  import org.apache.ibatis.logging.LogFactory;
23  import org.apache.ibatis.mapping.Environment;
24  import org.apache.ibatis.session.ExecutorType;
25  import org.apache.ibatis.session.SqlSession;
26  import org.apache.ibatis.session.SqlSessionFactory;
27  import org.mybatis.spring.transaction.SpringManagedTransactionFactory;
28  import org.springframework.dao.DataAccessException;
29  import org.springframework.dao.TransientDataAccessResourceException;
30  import org.springframework.dao.support.PersistenceExceptionTranslator;
31  import org.springframework.jdbc.datasource.DataSourceUtils;
32  import org.springframework.transaction.support.TransactionSynchronizationAdapter;
33  import org.springframework.transaction.support.TransactionSynchronizationManager;
34  
35  /**
36   * Handles MyBatis SqlSession life cycle. It can register and get SqlSessions from
37   * Spring {@code TransactionSynchronizationManager}. Also works if no transaction is active.
38   *
39   * @author Hunter Presnall 
40   * @author Eduardo Macarron
41   *
42   * @version $Id$
43   */
44  public final class SqlSessionUtils {
45  
46    private static final Log LOGGER = LogFactory.getLog(SqlSessionUtils.class);
47  
48    private static final String NO_EXECUTOR_TYPE_SPECIFIED = "No ExecutorType specified";
49    private static final String NO_SQL_SESSION_FACTORY_SPECIFIED = "No SqlSessionFactory specified";
50    private static final String NO_SQL_SESSION_SPECIFIED = "No SqlSession specified";
51  
52    /**
53     * This class can't be instantiated, exposes static utility methods only.
54     */
55    private SqlSessionUtils() {
56      // do nothing
57    }
58  
59    /**
60     * Creates a new MyBatis {@code SqlSession} from the {@code SqlSessionFactory}
61     * provided as a parameter and using its {@code DataSource} and {@code ExecutorType}
62     *
63     * @param sessionFactory a MyBatis {@code SqlSessionFactory} to create new sessions
64     * @return a MyBatis {@code SqlSession}
65     * @throws TransientDataAccessResourceException if a transaction is active and the
66     *             {@code SqlSessionFactory} is not using a {@code SpringManagedTransactionFactory}
67     */
68    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory) {
69      ExecutorType executorType = sessionFactory.getConfiguration().getDefaultExecutorType();
70      return getSqlSession(sessionFactory, executorType, null);
71    }
72  
73    /**
74     * Gets an SqlSession from Spring Transaction Manager or creates a new one if needed.
75     * Tries to get a SqlSession out of current transaction. If there is not any, it creates a new one.
76     * Then, it synchronizes the SqlSession with the transaction if Spring TX is active and
77     * <code>SpringManagedTransactionFactory</code> is configured as a transaction manager.
78     *
79     * @param sessionFactory a MyBatis {@code SqlSessionFactory} to create new sessions
80     * @param executorType The executor type of the SqlSession to create
81     * @param exceptionTranslator Optional. Translates SqlSession.commit() exceptions to Spring exceptions.
82     * @throws TransientDataAccessResourceException if a transaction is active and the
83     *             {@code SqlSessionFactory} is not using a {@code SpringManagedTransactionFactory}
84     * @see SpringManagedTransactionFactory
85     */
86    public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
87  
88      notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
89      notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
90  
91      SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
92  
93      SqlSession session = sessionHolder(executorType, holder);
94      if (session != null) {
95        return session;
96      }
97  
98      if (LOGGER.isDebugEnabled()) {
99        LOGGER.debug("Creating a new SqlSession");
100     }
101 
102     session = sessionFactory.openSession(executorType);
103 
104     registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
105 
106     return session;
107   }
108 
109   /**
110    * Register session holder if synchronization is active (i.e. a Spring TX is active).
111    *
112    * Note: The DataSource used by the Environment should be synchronized with the
113    * transaction either through DataSourceTxMgr or another tx synchronization.
114    * Further assume that if an exception is thrown, whatever started the transaction will
115    * handle closing / rolling back the Connection associated with the SqlSession.
116    * 
117    * @param sessionFactory sqlSessionFactory used for registration.
118    * @param executorType executorType used for registration.
119    * @param exceptionTranslator persistenceExceptionTranslater used for registration.
120    * @param session sqlSession used for registration.
121    */
122   private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType,
123       PersistenceExceptionTranslator exceptionTranslator, SqlSession session) {
124     SqlSessionHolder holder;
125     if (TransactionSynchronizationManager.isSynchronizationActive()) {
126       Environment environment = sessionFactory.getConfiguration().getEnvironment();
127 
128       if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) {
129         if (LOGGER.isDebugEnabled()) {
130           LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]");
131         }
132 
133         holder = new SqlSessionHolder(session, executorType, exceptionTranslator);
134         TransactionSynchronizationManager.bindResource(sessionFactory, holder);
135         TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory));
136         holder.setSynchronizedWithTransaction(true);
137         holder.requested();
138       } else {
139         if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) {
140           if (LOGGER.isDebugEnabled()) {
141             LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional");
142           }
143         } else {
144           throw new TransientDataAccessResourceException(
145               "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization");
146         }
147       }
148     } else {
149       if (LOGGER.isDebugEnabled()) {
150         LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active");
151       }
152     }
153 }
154 
155   private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) {
156     SqlSession session = null;
157     if (holder != null && holder.isSynchronizedWithTransaction()) {
158       if (holder.getExecutorType() != executorType) {
159         throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction");
160       }
161 
162       holder.requested();
163 
164       if (LOGGER.isDebugEnabled()) {
165         LOGGER.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction");
166       }
167 
168       session = holder.getSqlSession();
169     }
170     return session;
171   }
172 
173   /**
174    * Checks if {@code SqlSession} passed as an argument is managed by Spring {@code TransactionSynchronizationManager}
175    * If it is not, it closes it, otherwise it just updates the reference counter and
176    * lets Spring call the close callback when the managed transaction ends
177    *
178    * @param session
179    * @param sessionFactory
180    */
181   public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) {
182     notNull(session, NO_SQL_SESSION_SPECIFIED);
183     notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
184 
185     SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
186     if ((holder != null) && (holder.getSqlSession() == session)) {
187       if (LOGGER.isDebugEnabled()) {
188         LOGGER.debug("Releasing transactional SqlSession [" + session + "]");
189       }
190       holder.released();
191     } else {
192       if (LOGGER.isDebugEnabled()) {
193         LOGGER.debug("Closing non transactional SqlSession [" + session + "]");
194       }
195       session.close();
196     }
197   }
198 
199   /**
200    * Returns if the {@code SqlSession} passed as an argument is being managed by Spring
201    *
202    * @param session a MyBatis SqlSession to check
203    * @param sessionFactory the SqlSessionFactory which the SqlSession was built with
204    * @return true if session is transactional, otherwise false
205    */
206   public static boolean isSqlSessionTransactional(SqlSession session, SqlSessionFactory sessionFactory) {
207     notNull(session, NO_SQL_SESSION_SPECIFIED);
208     notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
209 
210     SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
211 
212     return (holder != null) && (holder.getSqlSession() == session);
213   }
214 
215   /**
216    * Callback for cleaning up resources. It cleans TransactionSynchronizationManager and
217    * also commits and closes the {@code SqlSession}.
218    * It assumes that {@code Connection} life cycle will be managed by
219    * {@code DataSourceTransactionManager} or {@code JtaTransactionManager}
220    */
221   private static final class SqlSessionSynchronization extends TransactionSynchronizationAdapter {
222 
223     private final SqlSessionHolder holder;
224 
225     private final SqlSessionFactory sessionFactory;
226 
227     private boolean holderActive = true;
228 
229     public SqlSessionSynchronization(SqlSessionHolder holder, SqlSessionFactory sessionFactory) {
230       notNull(holder, "Parameter 'holder' must be not null");
231       notNull(sessionFactory, "Parameter 'sessionFactory' must be not null");
232 
233       this.holder = holder;
234       this.sessionFactory = sessionFactory;
235     }
236 
237     /**
238      * {@inheritDoc}
239      */
240     @Override
241     public int getOrder() {
242       // order right before any Connection synchronization
243       return DataSourceUtils.CONNECTION_SYNCHRONIZATION_ORDER - 1;
244     }
245 
246     /**
247      * {@inheritDoc}
248      */
249     @Override
250     public void suspend() {
251       if (this.holderActive) {
252         if (LOGGER.isDebugEnabled()) {
253           LOGGER.debug("Transaction synchronization suspending SqlSession [" + this.holder.getSqlSession() + "]");
254         }
255         TransactionSynchronizationManager.unbindResource(this.sessionFactory);
256       }
257     }
258 
259     /**
260      * {@inheritDoc}
261      */
262     @Override
263     public void resume() {
264       if (this.holderActive) {
265         if (LOGGER.isDebugEnabled()) {
266           LOGGER.debug("Transaction synchronization resuming SqlSession [" + this.holder.getSqlSession() + "]");
267         }
268         TransactionSynchronizationManager.bindResource(this.sessionFactory, this.holder);
269       }
270     }
271 
272     /**
273      * {@inheritDoc}
274      */
275     @Override
276     public void beforeCommit(boolean readOnly) {
277       // Connection commit or rollback will be handled by ConnectionSynchronization or
278       // DataSourceTransactionManager.
279       // But, do cleanup the SqlSession / Executor, including flushing BATCH statements so
280       // they are actually executed.
281       // SpringManagedTransaction will no-op the commit over the jdbc connection
282       // TODO This updates 2nd level caches but the tx may be rolledback later on! 
283       if (TransactionSynchronizationManager.isActualTransactionActive()) {
284         try {
285           if (LOGGER.isDebugEnabled()) {
286             LOGGER.debug("Transaction synchronization committing SqlSession [" + this.holder.getSqlSession() + "]");
287           }
288           this.holder.getSqlSession().commit();
289         } catch (PersistenceException p) {
290           if (this.holder.getPersistenceExceptionTranslator() != null) {
291             DataAccessException translated = this.holder
292                 .getPersistenceExceptionTranslator()
293                 .translateExceptionIfPossible(p);
294             if (translated != null) {
295               throw translated;
296             }
297           }
298           throw p;
299         }
300       }
301     }
302 
303     /**
304      * {@inheritDoc}
305      */
306     @Override
307     public void beforeCompletion() {
308       // Issue #18 Close SqlSession and deregister it now
309       // because afterCompletion may be called from a different thread
310       if (!this.holder.isOpen()) {
311         if (LOGGER.isDebugEnabled()) {
312           LOGGER.debug("Transaction synchronization deregistering SqlSession [" + this.holder.getSqlSession() + "]");
313         }
314         TransactionSynchronizationManager.unbindResource(sessionFactory);
315         this.holderActive = false;
316         if (LOGGER.isDebugEnabled()) {
317           LOGGER.debug("Transaction synchronization closing SqlSession [" + this.holder.getSqlSession() + "]");
318         }
319         this.holder.getSqlSession().close();
320       }
321     }
322 
323     /**
324      * {@inheritDoc}
325      */
326     @Override
327     public void afterCompletion(int status) {
328       if (this.holderActive) {
329         // afterCompletion may have been called from a different thread
330         // so avoid failing if there is nothing in this one
331         if (LOGGER.isDebugEnabled()) {
332           LOGGER.debug("Transaction synchronization deregistering SqlSession [" + this.holder.getSqlSession() + "]");
333         }
334         TransactionSynchronizationManager.unbindResourceIfPossible(sessionFactory);
335         this.holderActive = false;
336         if (LOGGER.isDebugEnabled()) {
337           LOGGER.debug("Transaction synchronization closing SqlSession [" + this.holder.getSqlSession() + "]");
338         }
339         this.holder.getSqlSession().close();
340       }
341       this.holder.reset();
342     }
343   }
344 
345 }