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