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