MyBatis Caching

Categories: Java

MyBatis is a Java library that helps to map Java objects to/from SQL database tables. The MyBatis “local cache” is important yet hardly mentioned in the online MyBatis documentation; the following info is useful to know when working with MyBatis (version 3.2 or similar).

MyBatis has two layers of caching:

  • a “local cache” which is always enabled
  • a “second level cache” which is optional (enabled via a <cache> element in the config file)

The Local Cache

The local cache is always enabled, even when the main config file sets cacheEnabled=false or an individual statement sets useCache=false (these affect the second-level cache only). The only configuration setting that affects the local cache is:

  localCacheScope=SESSION|STATEMENT     (default = SESSION)

This local cache is a map where:

  • key = (mybatis-namespace + stmt-name) + (raw sql including parameter placeholders) + (actual SQL parameter values)
  • value = (list of Java objects resulting from that query)

When localCacheScope=STATEMENT then the cache is cleared at the end of each MyBatis statement.

When localCacheScope=SESSION, then the cache is cleared:

  1. at the end of the current transaction (or end of each statement when autoCommit=true)
  2. whenever any insert/update/delete statement is executed

The local cache data is stored in a member of a SimpleExecutor instance, which belongs to an SqlSession object.

When Spring transactions are enabled, a single SqlSession is shared between all MyBatis DAO instances that execute within that same transaction, and is discarded when the transaction completes. The sqlSession property of the DAO classes (which extend Spring’s SqlSessionDaoSupport) is actually an SqlSessionTemplate which retrieves the SqlSession object to use from the current transaction’s properties. Operations that do not run within a transaction create a temporary SqlSession. The result is that when localCacheScope=SESSION:

  • within the same transaction, executing the same SQL statement with the same parameters ends up returning a reference to the same Java object (and the DB query is not run)
  • objects are never shared between different transactions

The primary use for this cache is where a mapping uses sub-selects to populate collection properties; in this case, repeatly executing the same query simply returns a reference to the same object instance from the cache. Example:

    <resultMap id="ToolboxMap" type="Toolbox">
        <id property="id" column="ID" javaType="long"/>
        <result property="name" column="NAME" />
        <result property="colour" column="COLOUR"/>

        <collection property="screwDrivers" javaType="ArrayList" select="net.vonos.tools.Screwdrivers_query" column="ID" />
        <collection property="allenKeys" javaType="ArrayList" select="net.vonos.tools.AllenKeys_query" column="ID" />
        <collection property="drillBits" javaType="ArrayList" select="net.vonos.tools.DrillBits_query" column="ID" />
    </resultMap>

A secondary use is when a method uses transactions (either explicitly or via annotations) to perform multiple database operations within a single transaction, and can potentially execute the same MyBatis select statement with the same parameters multiple times.

The local cache key includes the raw SQL that would be sent to the database plus the actual parameter values that would be sent to the database. In other words, any “executable” expressions in the MyBatis mapping such as the following are first expanded before being used in the local cache “key”. The following example is expanded (if-condition evaluated) and the resulting string is part of the key:

<where>
  id=#{id}
  <if test="foo != null'">
    and foo=#{foo}
  </if>
</where>

Note in particular that repeatedly reading the same row from the database will return multiple references to the same object instance when within the same transaction, and references to different objects when not within the same transaction.

The Second-level Cache

When performing a query, the second-level cache is consulted first.

The second-level cache:

  • Shares data between transactions
  • By default, returns a copy of the cached object created via serialize/deserialize - which requires all cacheable objects to implement Serializable
  • Can hook into “enterprise” caching systems, or use a simple in-memory cache. The in-memory approach works only when the DB is exclusively updated by a single process
  • By default, flushes the entire cache whenever an <insert>, <update> or <delete> MyBatis statement is executed.
  • Keys cache entries by the same key for the local cache, ie a string built from (mybatis-namespace + stmt-name) + (raw sql) + (actual parameter values)

The second-level cache can return “shared” references to a common object if the “readOnly” attribute is set on the cache declaration. This should be used with care - if the returned object is modified by the caller then that modifies the object stored in the cache.

The second-level cache is effectively held at the MyBatis Configuration level. If you application follows a pattern where each DAO instance has its own MyBatis configuration object then the second-level cache does not share data between DAOs.

In order to share a cache between multiple MyBatis statements and multiple callers, the cache is locked for the period of execution of a MyBatis statement. There are separate read and write locks (ie multiple callers can concurrently lock-for-read, and a writer must wait until all readers release their locks).

Summary

Because the local cache only stores data for the duration of a transaction (at most) it is simple and effective. However it cannot optimise access to read-only or mostly-read-only data. The second-level cache can optimise access to such data, but must be very carefully configured to suit the access-patterns in the deployed environment otherwise it may return stale data. The second-level cache copies objects unless attribute readOnly=true is set.