What is Mybatis?

MyBatis is an open source project of Apache called iBatis. In 2010, this project was migrated to Google Code by Apache Software Foundation and renamed as MyBatis. Migrated to Github in November 2013. The term iBATIS comes from the combination of “Internet” and “Abatis”. IBATIS is a Java-based persistence layer framework.

IBATIS provides persistence layer frameworks including SQL Maps and Data Access Objects (DAOs)

Sorted out some learning materials of Mybatis, friends who need them can click to get them directly.

  • Mybatis technology insider
  • Mybatis source code analysis
  • Mybatis technology principle and actual combat
  • Related video: Mybatis open source framework source in-depth analysis

MyBatis cache details

Caching is a common feature of ORM frameworks to improve query efficiency and reduce database stress. Like Hibernate, MyBatis has level 1 and level 2 caches, and has an interface for integrating third-party caches.

Cache architecture:

MyBatis’ cache-related classes are in the cache package, which has a cache interface and only a default implementation class PerpetualCache, implemented using HashMap. We can find out what this cache looks like in the following classes

DefaultSqlSession

BaseExecutor

PerpetualCache localCache

private Map<Object, Object> cache = new HashMap();

In addition, there are many decorators through which you can implement many additional functions: recycle policies, logging, timed refreshes, and so on. But no matter how you decorate it, and how many layers you decorate, you end up with the basic implementation class (default PerpetualCache). You can view this through the CachingExecutor class Debug.

All cache implementation classes can be generally divided into three categories: basic cache, obsolete algorithm cache, and decorator cache.

Level 1 cache (local cache) :

Level 1 cache is also called local cache, MyBatis level 1 cache is cached at the session level (SqlSession). MyBatis level 1 cache is enabled by default and does not require any configuration. The first thing we have to figure out is that there are so many objects involved in the process being performed by MyBatis. Which object should the cache PerpetualCache be maintained in? If you want to share level 1 cache in the same session, this object must be created in SqlSession as a property of SqlSession.

There are only two properties in DefaultSqlSession. Configuration is global. So the cache may only inside the Executor – SimpleExecutor ReuseExecutor/in the constructor of the superclass BaseExecutor BatchExecutor hold PerpetualCache. If the same SQL statement is executed multiple times in the same session, the cache results are directly retrieved from the internal database, and the SQL is not sent to the database. But even if the same SQL is executed in different sessions (called with the same arguments from the same method in a Mapper), level 1 caching cannot be used.

MyBatis will create a SqlSession object to represent a database session whenever we open a session with MyBatis.

In a session on the database, we may be repeatedly execute the same query, if you don’t take some measures, each new query query a database, and we are in a very short period of time to do the exact same query, so they are most likely the result of the same, because the cost of query a database is very big, This can be a huge waste of resources.

In order to solve this problem, reduce the waste of resources, MyBatis will be in session said SqlSession objects to create a simple cache, cached will each query to the results of the results, the next time the query, if the judge was exactly the same query, will take out the results directly from the cache, returned to the user. There is no need for another database query.

As shown in the following figure, MyBatis will create a local cache in a session representation —- a SqlSession object. For each query, it will try to find whether it is in the local cache according to the query conditions. If it is in the cache, it will directly fetch it from the cache. It is then returned to the user; Otherwise, the data is read from the database, the query results are cached and returned to the user.

What is the lifetime of level 1 cache?

  1. When MyBatis starts a database session, it creates a new SqlSession object, which has a new Executor object, which holds a new PerpetualCache object. When the session ends, the SqlSession object and its internal Executor and PerpetualCache objects are also released.
  2. If the SqlSession calls the close() method, the Tier 1 cache PerpetualCache object is freed and the tier 1 cache is not available;
  3. If a SqlSession calls clearCache(), the data in the PerpetualCache object is cleared, but the object is still usable;
  4. Every UPDATE operation performed in SqlSession (update(), delete(), INSERT ()) clears data from the PerpetualCache object, but the object continues to be used;

SqlSession Level 1 cache workflow:

  1. For a query, according to the statementId, params, rowBounds to build a key value, according to the key value to Cache the Cache to retrieve the corresponding key value store Cache the results

  2. Determines whether the data obtained from the Cache based on the specified key value is null, that is, whether the data is hit.

  3. If a match is hit, the cached result is returned directly.

  4. If not:

    1. Go to the database query data, get query results;
    2. The key and the queried result are stored in the Cache as the key pair and value pair respectively.
    3. Return the query result;

Let’s verify that MyBatis level 1 cache can only be shared within a session and that the same data can be manipulated across sessions (different sessions). Check whether the CACHE is hit. If the SQL is sent to the database for execution again, the cache is not hit. If the object is printed directly, the result is fetched from the memory cache.

1. Share in the same session (different sessions cannot share)

. / / the same Session SqlSession session1 = sqlSessionFactory openSession (); BlogMapper mapper1 = session1.getMapper(BlogMapper.class); System.out.println(mapper1.selectBlogById(1002)); System.out.println(mapper1.selectBlogById(1002));Copy the code

Execute the following SQL above we can see the console print information (need to configure mybatis. Configuration. The log – impl = org. Apache. The ibatis. Logging. The stdout. StdOutImpl), We can see that our two queries send one query to the database, which shows that caching works:

PS: The first level cache is stored in the BaseExecutor query() — queryFromDatabase(). Get () comes before queryFromDatabase().

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing a The query "). The object (Ms. GetId ()); . try { ++this.queryStack; List = resultHandler == null? (List)this.localCache.getObject(key) : null; if (list ! = null) { this.handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); } else {list = this.queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); }... }Copy the code

2. In the same session, Update (including DELETE) causes level-1 cache to be cleared

. / / the same Session SqlSession session1 = sqlSessionFactory openSession (); BlogMapper mapper1 = session1.getMapper(BlogMapper.class); System.out.println(mapper1.selectBlogById(1002)); Blog blog3 = new Blog(); blog3.setBid(1002); Blog3. setName(" Mybatis cache mechanism modified "); mapper1.updateBlog(blog3); session1.commit(); System.out.println(mapper1.selectBlogbyId (1002)));Copy the code

Level 1 caching is cleared (unconditionally) by calling clearLocalCache() in the Update () method in BaseExecutor, as determined in Query.

public int update(MappedStatement ms, Object parameter) throws SQLException { ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); if (this.closed) { throw new ExecutorException("Executor was closed."); } else {// Clear the local cache this.clearlocalCache (); return this.doUpdate(ms, parameter); }}Copy the code

3. Other sessions update data, causing dirty data to be read (Level-1 cache cannot be shared across sessions)

SqlSession session1 = sqlSessionFactory.openSession(); BlogMapper mapper1 = session1.getMapper(BlogMapper.class); SqlSession session2 = sqlSessionFactory.openSession(); BlogMapper mapper2 = session2.getMapper(BlogMapper.class); System.out.println(mapper2.selectBlogById(1002)); Blog blog3 = new Blog(); blog3.setBid(1002); Blog3. setName(" Mybatis caching mechanism 1"); mapper1.updateBlog(blog3); session1.commit(); System.out.println(mapper2.selectBlogById(1002));Copy the code

Deficiencies of level 1 cache:

When using level 1 caching, because caches cannot be shared across sessions, different sessions may have different caches for the same data. In multiple sessions or distributed environments, there is the problem of dirty data. If you want to solve this problem, you use a level 2 cache. MyBatis Level 1 Cache (called Local Cache by MyBaits) cannot be turned off, but two levels are available:

  1. Session-level cache, in the same sqlSession, the same query will not query the database, directly from the cache.
  2. Statement level cache, avoiding pitfalls: To avoid this problem, you can set the level 1 cache to statement level so that the level 1 cache is flushed at the end of each query.

Level 2 cache:

Level 2 cache is used to solve the problem that level 1 cache cannot be shared across sessions, the scope is namespace level, can be shared by multiple SQLSessions (as long as the same method in the same interface, can be shared), life cycle and application synchronization. If MyBatis uses level 2 cache and Mapper and SELECT statements are configured to use level 2 cache, MyBatis will select data from level 2 cache first and then from Level 1 cache. Level 2 Cache – > Level 1 cache – > database.

As a broader cache, it must be in the outer layer of the SqlSession, otherwise it cannot be shared by multiple SQLSessions. Level 1 cache is stored in the SqlSession, so the first problem is that it must work before level 1 cache, that is, it must fetch level 1 cache in a session if level 2 cache is not available. Second question, in which object is the level 2 cache maintained? To share across sessions, SqlSession and its BaseExecutor are no longer sufficient, so we should create an object outside of the BaseExecutor.

MyBatis is actually maintained with a decorator class called CachingExecutor. If level 2 caching is enabled, MyBatis decorates Executor objects when they are created. CachingExecutor determines whether the level 2 cache has cached results for a query request and returns them directly. If there is no delegate to a real queryer Executor implementation class, such as SimpleExecutor, to execute the query and then go to level 1 cache. Finally, the results are cached and returned to the user.

Method to enable level 2 caching

Step 1: configure mybatis. Configuration. The cache – enabled = true, if not explicitly set cacheEnabled = false, all decorated with CachingExecutor basic actuators.

Step 2: Configure the tag in mapper.xml:

<cache type="org.apache.ibatis.cache.impl.PerpetualCache"
    size="1024"
eviction="LRU"
flushInterval="120000"
readOnly="false"/>
Copy the code

That’s basically it. This simple statement has the following effect:

  • The results of all SELECT statements in the mapping statement file will be cached.
  • All INSERT, UPDATE, and DELETE statements in the mapping statement file flush the cache.
  • The cache uses the Least Recently Used algorithm (LRU) algorithm to clear unwanted caches.
  • The cache is not flushed regularly (that is, there are no flush intervals).
  • The cache holds 1024 references to lists or objects (whichever is returned by the query method).
  • The cache is treated as a read/write cache, which means that the retrieved object is not shared and can be safely modified by the caller without interfering with potential changes made by other callers or threads.

This more advanced configuration creates a FIFO cache, refreshed every 60 seconds, that can store up to 512 references to result objects or lists, and the returned objects are considered read-only, so modifying them could cause conflicts among callers in different threads. The cleanup policies available are:

  • LRU– Least Recently used: Removes the object that has not been used for the longest time.
  • FIFO– First in, first out: Removes objects in the order in which they enter the cache.
  • SOFT– Soft reference: Removes objects based on the garbage collector status and soft reference rules.
  • WEAK– Weak references: Objects are removed more aggressively based on garbage collector state and weak reference rules.

The default cleanup policy is LRU.

The flushInterval property can be set to any positive integer, and the value should be a reasonable amount of time in milliseconds. The default is no, that is, there is no refresh interval, and the cache is flushed only when the statement is called.

The size attribute can be set to any positive integer, taking into account the size of the object to be cached and the memory resources available in the runtime environment. The default value is 1024.

The readOnly property can be set to true or false. A read-only cache returns the same instance of the cache object to all callers. Therefore, these objects cannot be modified. This provides a significant performance boost. A read-write cache returns (through serialization) a copy of the cached object. It’s slower, but safer, so the default is false.

Note: Level 2 caching is transactional. This means that the cache is updated when the SqlSession completes and commits, or when the INSERT/DELETE /update statement from flushCache=true is completed and rolled back.

After mapper.xml is configured, select() is cached. Update (), delete(), and insert() flush the cache. : If cacheEnabled=true, mapper.xml is not configured with tags, is there a second level cache? Will there still be a CachingExecutor wrapper object? (would)

The base executor is decorated as long as cacheEnabled= True. Configuration determines whether the Mapper Cache object is created at startup, but ultimately affects the decision in the Cache ExecutorQuery method. What if some query methods have high requirements for real-time data and do not require a level 2 cache? We can explicitly disable level 2 caching on a single Statement ID (default: true) :

<select id="selectBlog" resultMap="BaseResultMap" useCache="false">
Copy the code

Level 2 Cache verification (Level 2 cache must be enabled to verify level 2 cache)

1, transaction not committed, level 2 cache does not exist

System.out.println(mapper1.selectBlogById(1002)); // session1.com MIT (); System.out.println(mapper2.selectBlogById(1002));Copy the code

Why are transactions not committed and level 2 caches not in effect? Because the second level cache using TransactionalCacheManager (TCM) to manage, and then call the getObject TransactionalCache, putObject () and commit () method, TransactionalCache now holds a real Cache object, such as the embellished PerpetualCache. In putObject, only entriesToAddOnCommit calls flushPendingEntries() to the cache when its commit() method is called. It is called when DefaultSqlSession calls commit().

Use different sessions and mapper to verify that the second level cache can exist across sessions to cancel the above commit() comment

3. Perform add, delete and change operations in other sessions to verify that the cache will be refreshed

System.out.println(mapper1.selectBlogById(1002)); Blog3 = new Blog(); blog3.setBid(1002); Blog3. setName(" Mybatis caching mechanism "); mapper1.updateBlog(blog3); session1.commit(); System.out.println(mapper2.selectBlogById(1002));Copy the code

Why does add, delete and change clear the cache? The Update () method of CachingExecutor calls flushCacheIfRequired(ms). IsFlushCacheRequired is the value of the flushCache channel from the tag. The default flushCache attribute for the add, delete, and change operation is true.

When is level 2 caching enabled?

Level-1 cache is enabled by default. Level-2 cache can be enabled only after configuration. So we have to ask the question, when is it necessary to turn on level 2 caching?

  1. Because all additions, deletions and changes will refresh the secondary cache, resulting in the secondary cache invalidation, so it is suitable for use in query-oriented applications, such as the query of historical transactions, historical orders. Otherwise the cache is meaningless.
  2. If there are operations on the same table in multiple namespaces, such as the Blog table, and the cache is flushed in one namespace and not in another, dirty data will be read. Therefore, it is recommended to use only a single table in a Mapper.

What if you want multiple namespaces to share a level 2 cache? The problem of cache sharing across namespaces can be solved using:

<cache-ref namespace="com.wuzz.crud.dao.DepartmentMapper" />
Copy the code

Cache-ref indicates a cache configuration that references another namespace. Operations on both namespaces use the same cache. You can use this option when there are few associated tables or tables can be grouped by service.

Note: In this case, multiple Mapper operations cause the cache to flush, and the cache is of little use.

Third-party cache level 2 cache

In addition to MyBatis built-in level 2 Cache, we can also customize the level 2 Cache by implementing the Cache interface. MyBatis provides some third-party cache integration methods, such as EhCache and Redis: github.com/mybatis/red… Here is no more introduction. Of course, we can also use a separate cache service, not MyBatis built-in level 2 cache.

Custom cache:

In addition to the custom caching approach described above, you can also completely override caching behavior by implementing your own caching or creating adapters for other third-party caching schemes.

<cache type="com.domain.something.MyCustomCache"/>
Copy the code

This example shows how to use a custom cache implementation. The class specified by the type attribute must implement the org.mybatis. Cache.Cache interface and provide a constructor that takes a String argument as id. This interface is one of many complex interfaces in the MyBatis framework, but the behavior is very simple.

public interface Cache {
  String getId();
  int getSize();
  void putObject(Object key, Object value);
  Object getObject(Object key);
  boolean hasKey(Object key);
  Object removeObject(Object key);
  void clear();
}
Copy the code

To configure your cache, simply add the public JavaBean attributes to your cache implementation and pass the values through the cache element. For example, the following example calls a method called setCacheFile(String File) on your cache implementation:

<cache type="com.domain.something.MyCustomCache">
  <property name="cacheFile" value="/tmp/my-custom-cache.tmp"/>
</cache>
Copy the code

You can use all the simple types for JavaBean properties and MyBatis will convert them. You can also use placeholders (such as ${cache.file}) to replace the values defined in the configuration file properties. As of version 3.4.2, MyBatis supports calling an initialization method after all properties have been set. If you want to use this feature, please in your custom caching class org. Apache. Ibatis. Builder. InitializingObject interface.

public interface InitializingObject {
  void initialize() throws Exception;
}
Copy the code

Note that the configuration of the cache and the cache instance are bound to the namespace of the SQL mapping file. Therefore, all statements and caches in the same namespace are bound together through the namespace. Each statement can customize how it interacts with the cache, or exclude them from the cache entirely, by using two simple attributes on each statement. By default, statements are configured like this:

<select ... flushCache="false" useCache="true"/>
<insert ... flushCache="true"/>
<update ... flushCache="true"/>
<delete ... flushCache="true"/>
Copy the code

Given that this is the default behavior, obviously you should never explicitly configure a statement in this way. But if you want to change the default behavior, just set the flushCache and useCache properties. For example, in some cases you might want the results of a particular SELECT statement to be excluded from the cache, or you might want a SELECT statement to clear the cache. Similarly, you might want some UPDATE statements to execute without flushing the cache.


Now that you’ve seen it, give it a “like” before you go! 🤞

end