English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

Analysis of the reason why session cache is not used after spring integrates mybatis

Because I have been using Spring integrated with MyBatis all the time, so I rarely use MyBatis session cache. Habits are to use local cache with map or introduce a third-party local cache framework such as ehcache, Guava

So I'm a bit confused

Let's experiment first (Spring integration with MyBatis is omitted, there are many on the Internet), let's first look at the session cache at the MyBatis level

Enable printing SQL statements

Add to configuration.xml

<settings>
    <!-- Print the query statement -->
    <setting name="logImpl" value="STDOUT_LOGGING" />
  </settings>

The test source code is as follows:

DAO class 

/**
 * Testing why MyBatis cache is not working in Spring
 *
 * @author He Jinbin 2017.02.15
 */
@Component
public class TestDao {
  private Logger logger = Logger.getLogger(TestDao.class.getName());
  @Autowired
  private SqlSessionTemplate sqlSessionTemplate;
  @Autowired
  private SqlSessionFactory sqlSessionFactory;
  /**
   * two SQL
   *
   * @param id
   * @return
   */
  public TestDto selectBySpring(String id) {
    TestDto testDto = (TestDto) sqlSessionTemplate.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
    testDto = (TestDto) sqlSessionTemplate.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
    return testDto;
  }
  /**
   * one SQL
   *
   * @param id
   * @return
   */
  public TestDto selectByMybatis(String id) {
    SqlSession session = sqlSessionFactory.openSession();
    TestDto testDto = session.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
    testDto = session.selectOne("com.hejb.TestDto.selectByPrimaryKey", id);
    return testDto;
  }
}

Test service class

@Component
public class TestService {
  @Autowired
  private TestDao testDao;
  /**
   * Spring Mybatis query without transaction turned on
   */
  public void testSpringCashe() {
    //Executed two SQL statements
    testDao.selectBySpring("1;
  }
  /**
   * Spring Mybatis query with transaction turned on
   */
  @Transactional
  public void testSpringCasheWithTran() {
    //After spring transaction is turned on, query1the next SQL
    testDao.selectBySpring("1;
  }
  /**
   * mybatis query
   */
  public void testCash4Mybatise() {
    //Native mybatis, queried1the next SQL
    testDao.selectByMybatis("1;
  }
}

Output result:

The testSpringCashe() method executed two SQL statements, while others were executed once

Source code trace:

First look at the sqlSession in mybatis

Trace back to the call to the query method of org.apache.ibatis.executor.BaseExecutor

try {
   queryStack++;
   list = resultHandler == null63; (List<E>) localCache.getObject(key) : null; //First retrieve from the cache
   if (list != null) {
    handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); //Note that the key is CacheKey
   }
    list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
   }

Here is the code to retrieve the cached data

private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) {
  if (ms.getStatementType() == StatementType.CALLABLE) {
   final Object cachedParameter = localOutputParameterCache.getObject(key);//Retrieve the cached object from localOutputParameterCache
   if (cachedParameter != null && parameter != null) {
    final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter);
    final MetaObject metaParameter = configuration.newMetaObject(parameter);
    for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) {
     if (parameterMapping.getMode() != ParameterMode.IN) {
      final String parameterName = parameterMapping.getProperty();
      final Object cachedValue = metaCachedParameter.getValue(parameterName);
      metaParameter.setValue(parameterName, cachedValue);
     }
    }
   }
  }
 }

 

It is found that it is from localOutputParameterCache, which is a PerpetualCache, and PerpetualCache maintains a map, which is the essence of session caching.

Focus can be paid to the following two accumulation logic

PerpetualCache, two parameters, id and map

CacheKey, the key stored in the map, it has an overridden equas method, which is called when the cache is accessed.

The disadvantages of this local map cache object acquisition, based on my experience of falling into traps (I also used map to implement local caching before), is that the acquired objects are not cloned, and the two returned objects are the same address

In spring, it is usually used with sqlSessionTemplate, as follows

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    <property name="dataSource" ref="dataSource"> />
    <property name="configLocation" value="classpath:configuration.xml"> />
    <property name="mapperLocations">
      <list>
        <value>classpath*:com/hejb/sqlmap/*.xml</value>
      </list>
    </property>
  </bean>
  <bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    <constructor-arg ref="sqlSessionFactory" />
  </bean>

The sessions for executing SQL in SqlSessionTemplate are all through sqlSessionProxy, the generation of sqlSessionProxy is assigned in the constructor as follows:

this.sqlSessionProxy = (SqlSession) newProxyInstance(
    SqlSessionFactory.class.getClassLoader(),
    new Class[] { SqlSession.class },
    new SqlSessionInterceptor());

sqlSessionProxy is a proxy class generated by the JDK dynamic proxy method, the main logic is intercepted before and after the execution of the method in the InvocationHandler, the main logic is in the invoke method, including the creation, common, and closing of sqlsession for each execution

The code is as follows:

private class SqlSessionInterceptor implements InvocationHandler {
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
   // a new sqlSession is created before each execution
   SqlSession sqlSession = getSqlSession(
     SqlSessionTemplate.this.sqlSessionFactory,
     SqlSessionTemplate.this.executorType,
     SqlSessionTemplate.this.exceptionTranslator);
   try {
   // execute method
    Object result = method.invoke(sqlSession, args);
    if (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.this.sqlSessionFactory)) {
     // force commit even on non-dirty sessions because some databases require
     // a commit/rollback before calling close()
     sqlSession.commit(true);
    }
    return result;
   }
    Throwable unwrapped = unwrapThrowable(t);
    if (SqlSessionTemplate.this.exceptionTranslator != null && unwrapped instanceof PersistenceException) {
     // Release the connection to avoid a deadlock if the translator is not loaded. See issue #22
     closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
     sqlSession = null;
     Throwable translated = SqlSessionTemplate.this.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);
     if (translated != null) {
      unwrapped = translated;
     }
    }
    throw unwrapped;
   }
    if (sqlSession != null) {
     closeSqlSession(sqlSession, SqlSessionTemplate.this.sqlSessionFactory);
    }
   }
  }
 }

Since it is created every time, the cache of sqlSession is not used.

Why can it be used with a transaction that has been enabled, follow the getSqlSession method

The following is:

public static SqlSession getSqlSession(SqlSessionFactory sessionFactory, ExecutorType executorType, PersistenceExceptionTranslator exceptionTranslator) {
  notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);
  notNull(executorType, NO_EXECUTOR_TYPE_SPECIFIED);
  SqlSessionHolder holder = (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);
  // First, retrieve the session from the SqlSessionHolder
  SqlSession session = sessionHolder(executorType, holder);
  if (session != null) {
   return session;
  }
  if (LOGGER.isDebugEnabled()) {
   LOGGER.debug("Creating a new SqlSession");
  }
  session = sessionFactory.openSession(executorType);
  registerSessionHolder(sessionFactory, executorType, exceptionTranslator, session);
  return session;
 }

A SqlSessionHolder is maintained inside, which is associated with the transaction and session. If it exists, it will be directly retrieved, otherwise, a new session will be created. Therefore, in the transaction, each session is the same, so the cache can be used.

The following is the analysis of the reasons why session cache is not used after integrating Spring with MyBatis, which I introduced to everyone. I hope it will be helpful to everyone. If you have any questions, please leave a message, and I will reply to everyone in time. I am also very grateful for everyone's support of the Yell Tutorial website!

You May Also Like