Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Managing Query Performance with MyBatis Caching Layers

Tech May 19 1

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 SqlSession and can be cleared via sqlSession.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 Serializable if the cache is configured to persist or if read-only is set to false (safety copies rely on serialization).

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.