origin
In the process of using mybatispree, it is found that an error will be reported if an insert is executed and a batch insert is executed in a transaction
Cannot change the ExecutorType when there is an existing transaction
[the external link image transfer failed. The source station may have anti-theft chain mechanism. It is recommended to save the image and upload it directly (img-iFLCqrsB-1582688327549)(http://www.bxoon.com/upload/2020/2/image-9c94c608ea7b4e7e8afad042b0007564.png))
The reason for this is that the getSqlSession method in the sqlSessionHolder is called where the sqlsession is obtained. Why does this method cause this error?
SqlSessionHolder
sqlSessionHolder is located under the mybatis spring package. Its function is to control sqlSession and transactions
public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) { //Get sqlSessionHolder from threadLocal of the previous thread SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); //Call the static method sessionholder to determine whether there is a qualified sqlSession SqlSession session = sessionHolder(executorType, holder); // Determine whether the sqlSession is held in the current sqlSessionHolder (that is, whether the current operation is in the transaction) if (session != null) { //If the reference of sqlSesison is held, get it directly return session; } if (LOGGER.isDebugEnabled()) { LOGGER.debug("Creating a new SqlSession"); } //Gets the new sqlSession object. Here, the defaultSqlSession generated by sessionfactory session = sessionFactory.openSession(executorType); //Judge whether there is a transaction currently. Bind sqlSession to sqlSessionHolder and put it into threadload registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session); return session; }
This method mainly does the following things
- If the holder is not empty and the holder is locked by the transaction, you can obtain the sqlSession from the current transaction through the holder.getSqlSession() method, that is, the Fetched SqlSession from current transaction.
- If the holder does not exist or is not locked by the transaction, a new sqlSession will be created, that is, Creating a new SqlSession, through the sessionFactory.openSession() method.
- If the transaction of the current thread is active, it will register transaction synchronization for SqlSession, that is, Registering transaction synchronization for SqlSession.
private static SqlSession sessionHolder(ExecutorType executorType, SqlSessionHolder holder) { SqlSession session = null; if (holder != null && holder.isSynchronizedWithTransaction()) { //If the execution type saved by hodler is different from that obtained by sqlsession, an exception will be thrown. That is to say, in the same transaction, the execution type cannot be changed, because the sqlsession created by the same sqlSessionFactory in the same transaction will be reused if (holder.getExecutorType() != executorType) { throw new TransientDataAccessResourceException("Cannot change the ExecutorType when there is an existing transaction"); } //Increase the holder, that is, the unique sqlSession created by the same sqlSessionFactory in the same transaction. The number of references increases and the number of uses increases holder.requested(); if (LOGGER.isDebugEnabled()) { LOGGER.debug("Fetched SqlSession [" + holder.getSqlSession() + "] from current transaction"); } //Return to sqlSession session = holder.getSqlSession(); } return session; }
The reason for error reporting lies in the above. When obtaining sqlsession, ExecutorType will be verified. If the execution type saved by hodler is inconsistent with that obtained by sqlsession, an exception will be thrown. That is to say, in the same transaction, the execution type cannot be changed, because the sqlsession created by the same sqlSessionFactory in the same transaction will be reused.
When we execute a single insert, the default ExecutorType is SIMPLE. But when we execute a BATCH insert, the ExecutorType becomes BATCH, so this error will be caused.
So what is ExecutorType? From the source code of mybatis, you can see that there are three types of this type
public enum ExecutorType { SIMPLE, REUSE, BATCH }
- SIMPLE
- REUSE
- BATCH
As for the differences among the three, you can read this article.
Let's move on to the registerSessionHolder method in the source code of SqlSessionHolder above
private static void registerSessionHolder(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator, SqlSession session) { SqlSessionHolder holder; //Judge whether the transaction exists if (TransactionSynchronizationManager.isSynchronizationActive()) { Environment environment = sessionFactory.getConfiguration().getEnvironment(); //Load the environment variable to determine whether the registered transaction manager is a SpringManagedTransaction, that is, Spring manages the transaction if (environment.getTransactionFactory() instanceof SpringManagedTransactionFactory) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Registering transaction synchronization for SqlSession [" + session + "]"); } holder = new SqlSessionHolder(session, executorType, exceptionTranslator); //If the current call is in a transaction, bind the holder to ThreadLocal //Add sessionFactory as key and hodler as value to the local cache ThreadLocal < map < object, Object > > resources managed by transactionsynchronization Manager TransactionSynchronizationManager.bindResource(sessionFactory, holder); //Add synchronization of holder and sessionfactory into the local thread cache ThreadLocal < set < transactionsynchronization > > synchronizations TransactionSynchronizationManager.registerSynchronization(new SqlSessionSynchronization(holder, sessionFactory)); //Set current holder and current transaction synchronization holder.setSynchronizedWithTransaction(true); //holder references + 1 holder.requested(); } else { if (TransactionSynchronizationManager.getResource(environment.getDataSource()) == null) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because DataSource is not transactional"); } } else { throw new TransientDataAccessResourceException( "SqlSessionFactory must be using a SpringManagedTransactionFactory in order to use Spring transaction synchronization"); } } } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("SqlSession [" + session + "] was not registered for synchronization because synchronization is not active"); } } }
When sqlSession closes the session, the closeSqlSession method of sqlSessionUtils is used. sqlSessionHolder also makes a judgment. If the callback is in the transaction, the number of references will be reduced and the session will not be closed. If there is no transaction in the callback, close the session directly
public static void closeSqlSession(SqlSession session, SqlSessionFactory sessionFactory) { notNull(session, NO_SQL_SESSION_SPECIFIED); notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED); SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory); //If the holder holds a reference to sqlSession, (i.e. the session has a transaction) if ((holder != null) && (holder.getSqlSession() == session)) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Releasing transactional SqlSession [" + session + "]"); } //Reduce the number of references held by the holder every time an sqlSession is executed holder.released(); } else { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Closing non transactional SqlSession [" + session + "]"); } //If there is no transaction in the callback, close the session directly session.close(); } }