In the previous article, we explained the SqlSession in more detail, and the methods that execute the SqlSession are actually left to Executor to execute. Executor as one of the important components of MyBatis, the Executor also has excellent design and complex details. In this article, we’ll take a closer look at Executor and explore how it works.

1. The Executor first meeting

Executor is an interface that contains methods for updates, queries, transactions, and so on. Each SqlSession object has an Executor object, and the SqlSession operation is performed by the Executor. The Executor interface has an abstract implementation of the BaseExecutor class, which defines template methods that are implemented by subclasses.

The Executor interface defines the following methods:

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;
  / / update
  int update(MappedStatement ms, Object parameter) throws SQLException;

  // First query the cache, then query the database
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  / / query
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  // Return the cursor object
  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  / / release the Statement
  List<BatchResult> flushStatements(a) throws SQLException;

  // Transaction commit
  void commit(boolean required) throws SQLException;

  // Roll back the transaction
  void rollback(boolean required) throws SQLException;

  // Create a key-value pair for the cache
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  // Whether the cache exists
  boolean isCached(MappedStatement ms, CacheKey key);

  // Clear level 1 cache
  void clearLocalCache(a);

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class
        targetType);

  // Get the transaction object
  Transaction getTransaction(a);

  void close(boolean forceRollback);

  boolean isClosed(a);

  void setExecutorWrapper(Executor executor);

}
Copy the code
1.1 Executor inheritance relationship

The Executor interface has two implementations, a BaseExecutor abstract class and a CachingExecutor implementation class. The BaseExecutor abstract class has four implementations: SimpleExecutor, BathExecutor, ReuseExecutor, and ClosedExecutor.

The BaseExecutor abstract class follows the design pattern of template methods and defines a number of template methods, known as abstract methods. The other methods of the Executor interface, BaseExecutor, have default implementations for cache management and transaction operations, making the implementation of the interface easier.

  protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;

  protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;

  protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException;

  protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
      throws SQLException;
Copy the code

Of the four Executor classes, SimpleExectutor is used by default. Each Executor has the following characteristics:

  1. SimpleExecutor: The default executor that creates a Statement object each time an UPDATE or select operation is performed, and then closes the Statement object.
  2. ReuseExecutor: a reusable executor that reuses Statement objects. When a SQL is executed for the first time, the Statement object is cached in the map cache of the key-value structure. On the next execution, the Statement object can be retrieved from the cache, reducing the number of repeated compilations and improving performance. Each SqlSession object has an Executor object, so the cache is SqlSession level, and when SqlSession is destroyed, the cache is also destroyed.
  3. BatchExecutor: a BatchExecutor. By default, MyBatis sends one SQL each time it executes an SQL. The batch executor, on the other hand, executes the SQL one at a time. Instead of sending the SQL to the database immediately, it sends the SQL in batches.

4.ClosedExecutor: the inner class of ResultLoaderMap for handling lazy load correlation, which will be discussed in another article.

1.2 Executor to create

How Executor is created? In the previous articles, we did not show reference to Executor, but SqlSession refers to Executor objects. When we get the SqlSession objects from SqlSessionFactory, will call to SqlSessionFactory openSession () method, actually call openSessionFromDataSource method, in this method, The transaction is created, and the Executor object is created.

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      final Environment environment = configuration.getEnvironment();
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      final Executor executor = configuration.newExecutor(tx, execType);
      return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
      closeTransaction(tx); // may have fetched a connection so lets call close()
      throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
    } finally{ ErrorContext.instance().reset(); }}Copy the code

Follow the configuration. NewExecutor (tx, execType), we continue to look in. To create different types of executors according to ExecutorType, the default is SimpleExecutor. If level-1 caching is enabled (by default), CachingExecutor is also used to wrap the SimpleExecutor executor, where decorator design pattern is used.

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
   // Load the plugin chain
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }
Copy the code

All three types of Executor are created by calling the BaseExecutor constructor, which involves the following initializations for transaction read celebrations, lazy loading, and caching. A SqlSession now has its own unique Executor object.

  protected BaseExecutor(Configuration configuration, Transaction transaction) {
    this.transaction = transaction;
    this.deferredLoads = new ConcurrentLinkedQueue<>();
    this.localCache = new PerpetualCache("LocalCache");
    this.localOutputParameterCache = new PerpetualCache("LocalOutputParameterCache");
    this.closed = false;
    this.configuration = configuration;
    this.wrapper = this;
  }
Copy the code
1.3 Executor implementation

When we call a method on the Mapper interface, we end up calling the SqlSession method. In the case of DefaultSqlSession, we end up calling the Executor.query() method. Executor is a puppet of the SqlSession.

  @Override
  public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      executor.query(ms, wrapCollection(parameter), rowBounds, handler);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
    } finally{ ErrorContext.instance().reset(); }}Copy the code

2. SimpleExecutor

SimpleExecutor is the default executor and the simplest. It implements the four abstract methods defined by BaseExecutor, doUpdate, doQuery, doQueryCursor and doFlushStatements.

DoUpdate is used as an example to describe how SimpleExecutor works

  1. Create StatementHandler
  2. Create the Statement
  3. Performing SQL operations
  4. Closing Statement
@Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 1. Create a StatementHandler
      StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null.null);
      // 2. Create a Statement
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 3. Perform SQL operations
      return handler.update(stmt);
    } finally {
      // 2. Close StatementcloseStatement(stmt); }}Copy the code

StatementHandler is used to manage the Statement object in JDBC and to operate with the database. StatementHandler and Statement are discussed in other articles and will not be discussed here.

3. ReuseExecutor

ReuseExecutor is a reuse of Statement objects. If you execute a SQL multiple times in a SqlSession, it can be a waste of resources to generate the Statement object each time. ReuseExecutor therefore improves the prepareStatement() method on SimpleExecutor by caching the Statement object in memory and eliminating the fourth step: closing the Statement object.

  @Override
  public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null.null);
    Statement stmt = prepareStatement(handler, ms.getStatementLog());
    return handler.update(stmt);
  }
Copy the code

PrepareStatement does four steps:

  1. Whether the Sql matched the cache
  2. The Statement object is retrieved directly from the cache
  3. If it is not in the cache, a new Statement object is created
  4. Step 3: Place SQL as key and Statement as value into cache
private final Map<String, Statement> statementMap = new HashMap<>();

/** * Get the Statement object *@param handler
   * @param statementLog
   * @return
   * @throws SQLException
   */
  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    BoundSql boundSql = handler.getBoundSql();
    String sql = boundSql.getSql();
    / / 1. SQL exists in the cache
    if (hasStatementFor(sql)) {
      // 2. Hit the cache directly from the cache
      stmt = getStatement(sql);
      applyTransactionTimeout(stmt);
    } else {
      Connection connection = getConnection(statementLog);
      // 3. Create a new Statement object
      stmt = handler.prepare(connection, transaction.getTimeout());
      // 4. Place SQL as key and Statement as value in the cache
      putStatement(sql, stmt);
    }
    handler.parameterize(stmt);
    return stmt;
  }

  /** * Cache whether the Statement object * exists in the map@param sql
   * @return* /
  private boolean hasStatementFor(String sql) {
    try {
      Statement statement = statementMap.get(sql);
      returnstatement ! =null && !statement.getConnection().isClosed();
    } catch (SQLException e) {
      return false; }}/** * get * from cache@param s
   * @return* /
  private Statement getStatement(String s) {
    return statementMap.get(s);
  }

  /** * Put SQL as key and Statement as value in cache *@param sql
   * @param stmt
   */
  private void putStatement(String sql, Statement stmt) {
    statementMap.put(sql, stmt);
  }
Copy the code

Since SQLSessions reference their own Executor objects, the cache is at the SqlSession level, and if the SqlSession is destroyed, the corresponding cache will also be destroyed.

4. BatchExecutor

BatchExecutor is a bit more complicated. Batching means sending SQL to the database in batches, rather than one by one. DoUpdate (), for example, stores multiple Statement objects into a List, and then puts the results of the execution into a List. When the doUpdate method is executed, SQL does not execute immediately. Instead, SQL waits until commit or ROLLBACK to execute JDBC’s executeBatch method.

  1. DoUpdate () returns a fixed value, not the number of rows affected
  2. If the same SQL is committed consecuently, it will only be executed once
  3. The commit SQL is not executed immediately, but is executed uniformly at the commit time
  4. The underlying use is JDBC batch operations, addBatch() and executeBatch().
// Batch update processing returns a fixed value, not the number of rows affected
  public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;

  / / collection Statement
  private final List<Statement> statementList = new ArrayList<>();
  // Batch result set
  private final List<BatchResult> batchResultList = new ArrayList<>();
  // The last Sql statement
  private String currentSql;
  // The last MappedStatement object
  private MappedStatement currentStatement;

  public BatchExecutor(Configuration configuration, Transaction transaction) {
    super(configuration, transaction);
  }

  @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null.null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    // 1. Initialize currentSql and currentStatement to NULL, and take the else branch
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);// fix Issues 322
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      handler.parameterize(stmt);    // fix Issues 322
      // 2. Set currentSql and currentStatement to the currentSql
      currentSql = sql;
      currentStatement = ms;
      // 3. Add Statement to list.
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // 4. Call the addBatch() method of JDBC to add to the batch
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }
Copy the code

In doFlushStatements, statements in the statementList collection are traversed, executed one by one, and the result is loaded into the result set.

@Override
  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<>();
      // 1. The rollback is directly returned
      if (isRollback) {
        return Collections.emptyList();
      }
      // 2. Traversal the statementList collection
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          // 3. Execute the SQL
          batchResult.setUpdateCounts(stmt.executeBatch());
          MappedStatement ms = batchResult.getMappedStatement();
          List<Object> parameterObjects = batchResult.getParameterObjects();
          KeyGenerator keyGenerator = ms.getKeyGenerator();
          if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
            Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
            jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
          } else if(! NoKeyGenerator.class.equals(keyGenerator.getClass())) {//issue #141
            for (Object parameter : parameterObjects) {
              keyGenerator.processAfter(this, ms, stmt, parameter); }}// Close statement to close cursor #1109
          // 4. Close Statement
          closeStatement(stmt);
        } catch (BatchUpdateException e) {
          StringBuilder message = new StringBuilder();
          message.append(batchResult.getMappedStatement().getId())
              .append(" (batch index #")
              .append(i + 1)
              .append(")")
              .append(" failed.");
          if (i > 0) {
            message.append("")
                .append(i)
                .append(" prior sub executor(s) completed successfully, but will be rolled back.");
          }
          throw new BatchExecutorException(message.toString(), e, results, batchResult);
        }
        // 5. Load the result into the result set
        results.add(batchResult);
      }
      return results;
    } finally {
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null; statementList.clear(); batchResultList.clear(); }}Copy the code

5. CachingExecutor

When creating an Executor object, you determine whether level1 caching is enabled. If it is, you use CachingExecutor to wrap one of the three types of Executor, using decorator design pattern to enhance the Executor’s caching capabilities.

    // Whether to enable level-1 cache. This function is enabled by default
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);
    }
Copy the code

The constructor for CachingExecutor is as follows

 // 1. Delegate actuator, that is, one of the three types of actuators that are wrapped
  private final Executor delegate;
  // 2. Cache management class to manage TransactionalCache
  private final TransactionalCacheManager tcm = new TransactionalCacheManager();

  public CachingExecutor(Executor delegate) {
    // 3. Cross-referencing
    this.delegate = delegate;
    delegate.setExecutorWrapper(this);
  }
Copy the code

When an update is performed, the cache is cleared before the actual executor’s update method is executed

@Override
  public int update(MappedStatement ms, Object parameterObject) throws SQLException {
    // Clear the cache
    flushCacheIfRequired(ms);
    // Call the update method of the actual executor
    return delegate.update(ms, parameterObject);
  }
Copy the code

When the query is executed, it is first fetched from the cache. If the cache is not available, the actual executor’s query method is called to query the database, and it is returned to the cache.

  1. Get cache key
  2. Query the cache and return a hit
  3. If it is not in the cache, or if the cache does not exist, the database is queried and put into the cache

TransactionalCacheManager and TransactionalCache involve the caching module, also plans to explain, in the other articles in this one has brought.

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // 1. Obtain the cache key
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

  @Override
  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, boundSql);
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        // 2. If the cache is empty, query the database and put it into the cache
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        returnlist; }}return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }
Copy the code

6. Executor chestnuts

There are four types of Executor, but there are three types that actually operate: SimpleExecutor, ReuseExecutor, and BatchExecutor. If you do not specify an Executor type, SimpleExecutor defaults. If caching is enabled, the CachingExecutor wrapper is used to add cache logic. The next step is to use a few chestnuts to actually operate, which can also show the characteristics of the three actuators.

6.1 SimpleExecutor

You can specify the type of executor when obtaining the sqlSession, first looking at the SimpleExecutor execution result.

public static void main(String[] args) {
        try {
            // 1. Read the configuration
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. Create SqlSessionFactory factory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. Obtain the sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. Obtain the Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. Execute the interface method
            for(long i = 1; i < 3; i++){
                TTestUser userInfo = userMapper.selectByPrimaryKey(i);
                System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
            }
            // 6
            sqlSession.commit();
            // 7. Close resources
            sqlSession.close();
            inputStream.close();
        } catch(Exception e){ log.error(e.getMessage(), e); }}Copy the code

You can see from the log print that we executed the SQL twice and the Statement was compiled twice.

6.2 ReuseExecutor

The Statement object is reused by the ReuseExecutor. The Statement object is cached in key-value format, which improves performance by avoiding multiple compilation of the same SQL.

public static void main(String[] args) {
        try {
            // 1. Read the configuration
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. Create SqlSessionFactory factory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. Obtain the sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.REUSE);
            // 4. Obtain the Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. Execute the interface method
            for(long i = 1; i < 3; i++){
                TTestUser userInfo = userMapper.selectByPrimaryKey(i);
                System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
            }
            // 6
            sqlSession.commit();
            // 7. Close resources
            sqlSession.close();
            inputStream.close();
        } catch(Exception e){ log.error(e.getMessage(), e); }}Copy the code

You can see from the log print that we executed the SQL twice and the Statement was compiled once.

6.3 BatchExecutor

A BatchExecutor simply commits the SQL at commit, rather than sending it once and executing it once.

 try {
            // 1. Read the configuration
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. Create SqlSessionFactory factory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. Obtain the sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
            // 4. Obtain the Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. Execute the interface method
            for(long i = 1; i < 3; i++){
                TTestUser userInfo = new TTestUser();
                userInfo.setMemberId(2000+i);
                userInfo.setNickname(2000+i+"_nick");
                userInfo.setRealName(2000+i+"_real");
                userMapper.insertSelective(userInfo);
                // Simulate insertion interval
                Thread.sleep(1000);
            }
            System.out.println("------- Start submitting transactions ---------");
            // 6
            sqlSession.commit();
            System.out.println("------- Closing submissions ---------");
            // 7. Close resources
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
Copy the code

This example is less obvious, but you can see that despite thread.sleep (1000), the BatchExecutor operation inserts data into the database at the same time as the final commit.

6.4 CachingExecutor

CachingExecutor does not perform specific updates and queries. Instead, the CachingExecutor clears the cache when the update is executed, looks up the cache when the query is executed, and returns a hit.

try {
            // 1. Read the configuration
            InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
            // 2. Create SqlSessionFactory factory
            SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
            // 3. Obtain the sqlSession
            SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);
            // 4. Obtain the Mapper
            TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class);
            // 5. Execute the interface method
            for(long i = 1; i < 3; i++){
                TTestUser userInfo = userMapper.selectByPrimaryKey(2001L);
                System.out.println("userInfo = " + JSONUtil.toJsonStr(userInfo));
            }
            // 6
            sqlSession.commit();
            // 7. Close resources
            sqlSession.close();
            inputStream.close();
        } catch (Exception e){
            log.error(e.getMessage(), e);
        }
Copy the code

In this case, we execute the SQL twice, but the parameters and statements are the same, so the results of the first query are cached, and the results of the second query are retrieved directly from the cache.

7. To summarize

This article introduces Executor interfaces, inheritance relationships, three types of executors, and a few examples to show how they are different. I hope you can comment in the comments section and make progress together.