In depth understanding of Mybatis architecture design

Keywords: Java MySQL Mybatis Back-end Programmer

architecture design

We can divide the functional architecture of Mybatis into three layers:

  1. API interface layer: interface APIs provided for external use. Developers use these local APIs to manipulate the database. As soon as the interface layer receives the call request, it will call the data processing layer to complete the specific data processing.

Mybatis interacts with the database in two ways:

  • Use the traditional Mybatis API
  • How to use Mapper proxy
  1. Data processing layer: responsible for specific SQL search, SQL parsing, SQL execution and execution result mapping processing. His main purpose is to complete a database operation according to the call request.
  2. Basic support layer: it is responsible for the most basic functional support, including connection management, transaction management, configuration loading and cache processing. These are common things, and they are extracted as the most basic components. Provide the most basic support for the upper data processing layer.

Main components of Mybatis

component

describe

SqlSession

As the main top-level API of Mybatis, it represents the session interacting with the database and completes the necessary functions of database addition, deletion, query and modification

Executor

Mybatis executor, the core of mybatis scheduling, is responsible for generating SQL statements and maintaining query cache

StatementHandler

It encapsulates the JDBC Statement operation and is responsible for the operation of JDBC Statement, such as setting parameters and converting the Statement result set into a List set

ParameterHandler

It is responsible for converting the parameters passed by the user into the parameters required by JDBC Statement

ResultSetHandler

It is responsible for converting the ResultSet result set object returned by JDBC to a List type collection

TypeHandler

Responsible for the mapping and conversion between java data types and jdbc data types

MappedStatement

MappedStatement maintains an encapsulation of < select, update, delete, Insert > nodes

SqlSource

It is responsible for dynamically generating SQL statements according to the parameterObject passed by the user and encapsulating the information into the BoundSql object

BoundSql

Represents dynamically generated SQL statements and corresponding parameter information

Overall process:

  1. Load configuration and initialize

The Configuration comes from two sources, one is the Configuration file (conf.xml,mapper*.xml) and the other is the annotation in java code. The content of the Configuration file is encapsulated in Configuration, and the Configuration information of sql is loaded into a mappedstatement object and stored in memory.
2. Receive call request

Trigger condition: call the API provided by Mybatis

Incoming parameters: the ID of the SQL and the incoming parameters

Pass the request to the lower request processing layer for processing

  1. Processing operation requests
  • Find the corresponding MappedStatement object according to the ID of SQL
  • According to the parsing of the incoming parameter object, the final SQL to be executed and the incoming parameters to be executed are obtained
  • Get the database connection, give the final SQL statement and parameters to the database for execution, and get the execution results
  • According to the result mapping configuration in the MappedStatement object, the obtained execution result is transformed and the final processing result is obtained
  • Release connection resources
  1. Return processing results

Mybatis cache

Mybatis has L1 cache and L2 cache. After receiving the query request, mybatis will first query the L2 cache. If the L2 cache misses, then query the L1 cache. If the L1 cache does not exist, then query the database.

L1 cache

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 query").object(ms.getId());
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
      clearLocalCache();
    }
    List<E> list;
    try {
      queryStack++;
      //Check the data from the localCache cache. If not, check the database
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    if (queryStack == 0) {
      for (DeferredLoad deferredLoad : deferredLoads) {
        deferredLoad.load();
      }
      // issue #601
      deferredLoads.clear();
      if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
        // issue #482
        clearLocalCache();
      }
    }
    return list;
  }

This localCache is a property in BaseExecutor

public abstract class BaseExecutor implements Executor {


  protected PerpetualCache localCache;

PerpetualCache class

public class PerpetualCache implements Cache {

  private final String id;

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

  public PerpetualCache(String id) {
    this.id = id;
  }

  @Override
  public String getId() {
    return id;
  }

  @Override
  public int getSize() {
    return cache.size();
  }

  @Override
  public void putObject(Object key, Object value) {
    cache.put(key, value);
  }

  @Override
  public Object getObject(Object key) {
    return cache.get(key);
  }

L2 cache

To enable L2 caching:

  1. Enable cacheEnabled (on by default)
<settings>
<setting name="cacheEnabled" value="true"/>
</settings>
  1. It needs to be added in the Mapper configuration file of L2 cache
<cache></cache>
  1. Note that the sqlSession.commit or close method must be called for the L2 cache to take effect
 public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    if (cache != null) {
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, parameterObject, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

Note that cache = ms.getcache();, This cache is obtained from the MappedStatement. Since the MappedStatement exists in the global configuration, it can be obtained by multiple cacheingexecutors, which will lead to thread safety problems. In addition, if not controlled, multiple transactions share a cache instance, which will lead to dirty reads.

So how does mybatis solve dirty reading? It is solved by borrowing the above variable tcm, that is, the TransactionalCacheManager class.

TransactionalCacheManager class maintains the relationship between Cache and TransactionalCache. The real data is handled by TransactionalCache.

The structure is shown in the figure:

public class TransactionalCacheManager {

  private final Map<Cache, TransactionalCache> transactionalCaches = new HashMap<Cache, TransactionalCache>();

  public void clear(Cache cache) {
    getTransactionalCache(cache).clear();
  }

  public Object getObject(Cache cache, CacheKey key) {
    return getTransactionalCache(cache).getObject(key);
  }
  
  public void putObject(Cache cache, CacheKey key, Object value) {
    getTransactionalCache(cache).putObject(key, value);
  }

  public void commit() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.commit();
    }
  }

  public void rollback() {
    for (TransactionalCache txCache : transactionalCaches.values()) {
      txCache.rollback();
    }
  }

  private TransactionalCache getTransactionalCache(Cache cache) {
    TransactionalCache txCache = transactionalCaches.get(cache);
    if (txCache == null) {
      txCache = new TransactionalCache(cache);
      transactionalCaches.put(cache, txCache);
    }
    return txCache;
  }

}

Next, let's look at the code of TransactionalCache

public class TransactionalCache implements Cache {

  private static final Log log = LogFactory.getLog(TransactionalCache.class);

  // Real cache object
  private final Cache delegate;
  private boolean clearOnCommit;
  //Before the transaction is committed, the results of all queries from the database will be cached in this collection
  private final Map<Object, Object> entriesToAddOnCommit;
  //Before the transaction is committed, when the cache misses, the CacheKey will be stored in this collection
  private final Set<Object> entriesMissedInCache;

  public TransactionalCache(Cache delegate) {
    this.delegate = delegate;
    this.clearOnCommit = false;
    this.entriesToAddOnCommit = new HashMap<Object, Object>();
    this.entriesMissedInCache = new HashSet<Object>();
  }

  @Override
  public String getId() {
    return delegate.getId();
  }

  @Override
  public int getSize() {
    return delegate.getSize();
  }

  @Override
  public Object getObject(Object key) {
    // issue #116
    //It is obtained from the delegate when obtaining the cache
    Object object = delegate.getObject(key);
    if (object == null) {
      //Cache miss, store key in entriesMissedInCache
      entriesMissedInCache.add(key);
    }
    // issue #146
    if (clearOnCommit) {
      return null;
    } else {
      return object;
    }
  }

  @Override
  public ReadWriteLock getReadWriteLock() {
    return null;
  }

  @Override
  public void putObject(Object key, Object object) {
    //When put, only the database data is put into the entriesToAddOnCommit
    entriesToAddOnCommit.put(key, object);
  }

  @Override
  public Object removeObject(Object key) {
    return null;
  }

  @Override
  public void clear() {
    clearOnCommit = true;
    entriesToAddOnCommit.clear();
  }

  public void commit() {
    if (clearOnCommit) {
      delegate.clear();
    }
    //Refresh the uncached results to the delegate
    flushPendingEntries();
    reset();
  }

  public void rollback() {
    unlockMissedEntries();
    reset();
  }

  private void reset() {
    clearOnCommit = false;
    entriesToAddOnCommit.clear();
    entriesMissedInCache.clear();
  }

  private void flushPendingEntries() {
    for (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {
      delegate.putObject(entry.getKey(), entry.getValue());
    }
    for (Object entry : entriesMissedInCache) {
      if (!entriesToAddOnCommit.containsKey(entry)) {
        delegate.putObject(entry, null);
      }
    }
  }

  private void unlockMissedEntries() {
    for (Object entry : entriesMissedInCache) {
      try {
        delegate.removeObject(entry);
      } catch (Exception e) {
        log.warn("Unexpected exception while notifiying a rollback to the cache adapter."
            + "Consider upgrading your cache adapter to the latest version.  Cause: " + e);
      }
    }
  }

}

When we store the L2 cache, we put it into the TransactionalCache.entriesToAddOnCommit map, but we query it from delegate every time, so the cache does not take effect immediately after the L2 cache queries the database. Only when the commit or close method of sqlSession is executed, it will call the commit of tcm. After calling the commit of transactionlCache, the cache will be refreshed to delegate.

Summary:

  • In the design of L2 cache, decorator modes are widely used, such as synchronized cache and logging cache.
  • The L2 cache realizes the cache data sharing between sqlsessions, which belongs to the namespace level
  • The implementation of L2 cache is completed by cacheingexecution and a transactional pre cache TransactionlCache.

There is a road to the mountain of books. Diligence is the path. There is no end to learning. It is hard to make a boat

Posted by killfall on Tue, 30 Nov 2021 07:38:13 -0800