Managing Query Performance with MyBatis Caching Layers
Session-Level Cache
Every SqlSession holds an internal cache that is active by default. This local storage avoids redundant database calls when identical queries run inside the same session.
Internal Mechanics
The cache is backed by a Map scoped to the session. When a query arrives, MyBatis checks this map before forwarding the request to the database. The relevant logic resides in the BaseExecutor:
public abstract class BaseExecutor implements Executor {
protected PerpetualCache sessionCache = new PerpetalCache("SqlSessionCache");
@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) {
List<E> cachedList = (List<E>) sessionCache.get(cacheKey);
if (cachedList != null) {
return cachedList;
}
List<E> freshList = queryDatabase(ms, parameter, rowBounds, resultHandler, boundSql);
sessionCache.put(cacheKey, freshList);
return freshList;
}
}
Usage Scenario
SqlSession session = factory.openSession();
try {
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
Employee empA = mapper.findById(55); // hits database
Employee empB = mapper.findById(55); // served from local cache
} finally {
session.close();
}
The second lookup never reaches the database, provided the first call completed within the same session.
Namespace-Level Cache
This shared cache bridges multiple sessions that use the same mapper interface. It must be explicitly turned on and survives individual session lifetimes.
Enabling the Global Cache
First, confirm the configuration setting:
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
Next, place a <cache/> element inside the desired mapper XML:
<mapper namespace="com.demo.mapper.EmployeeMapper">
<cache eviction="LRU" flushInterval="30000" size="256" readOnly="false"/>
</mapper>
Under the Hood
MyBatis decorates the executor with CachingExecutor, which manages a TransactionalCacheManager to coordinate cache entries across transactions:
public class CachingExecutor implements Executor {
private final Executor wrapped;
private final TransactionalCacheManager cacheManager = new TransactionalCacheManager();
@Override
public <E> List<E> query(MappedStatement ms, Object param, RowBounds rowBounds,
ResultHandler handler, CacheKey key, BoundSql boundSql) {
Cache namespaceCache = ms.getCache();
if (namespaceCache == null) {
return wrapped.query(ms, param, rowBounds, handler, key, boundSql);
}
flushIfRequired(ms);
if (!ms.isUseCache() || handler != null) {
return wrapped.query(ms, param, rowBounds, handler, key, boundSql);
}
List<E> cached = (List<E>) cacheManager.get(namespaceCache, key);
if (cached == null) {
cached = wrapped.query(ms, param, rowBounds, handler, key, boundSql);
cacheManager.put(namespaceCache, key, cached);
}
return cached;
}
}
Usage Scenario
// First session fetches and populates the shared cache
SqlSession sessionOne = factory.openSession();
try {
EmployeeMapper mapper = sessionOne.getMapper(EmployeeMapper.class);
mapper.findById(55);
sessionOne.commit(); // write to second-level cache happens here
} finally {
sessionOne.close();
}
// Second session leverages the existing cache entry
SqlSession sessionTwo = factory.openSession();
try {
EmployeeMapper mapper = sessionTwo.getMapper(EmployeeMapper.class);
Employee cached = mapper.findById(55); // retrieved from namespace cache
} finally {
sessionTwo.close();
}
The commit on the first session is mandatory; uncommitted work never reaches the shared cache.
Important Considerations
- The local cache is confined to one
SqlSessionand can be cleared viasqlSession.clearCache(). - The global cache requires explicit configuration and transaction commit before data is stored.
- Cached objects are copies, so they may diverge from live database records.
- Control eviction (
LRU,FIFO, etc.), flush interval, maximum size, and read-only mode through the<cache/>tag. - Exercise caution with shared caches when multiple writers update the same tables—stale reads become possible.
- Objects placed in the second-level cache must implement
Serializableif the cache is configured to persist or if read-only is set to false (safety copies rely on serialization).