SQL Execution of Mybatis Core Working Principles

SQL Execution of Mybatis Core Working Principles

Learn the structure, working principle and main modules of Mybatis from a macro perspective, and learn the working principle and design idea of Mybatis from a micro perspective.

1. SQL Execution

Let's look at the execution of Mybatis's SQL statement.

List<User> list = mapper.selectUserList();

As we mentioned earlier, all Mappers are JDK dynamic proxy objects, so any method is to execute the invoke() method of the management class MapperProxy.

1.MapperProxy.invoke()

	@Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      // Methods such as toString hashCode equals getClass do not require walking to the process of executing SQL
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else {
        // Increase the efficiency of getting mapperMethod to invoke MapperMethodInvoker
        // The normal method goes to invoke of PlainMethodInvoker
        return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }

Enter into the cachedInvoker method.

private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
    try {
      // Map's method in Java8, gets a value based on the key, and assigns the value of the following Object to the key if the value is null
      // If not, create
      // Getting the MapperMethodInvoker object, there is only one invoke method
      // Remove from the methodCache based on the method Fill in with the second parameter if empty is returned
      return methodCache.computeIfAbsent(method, m -> {
        if (m.isDefault()) {
          // The default method of the interface (Java8), which inherits the default method of the interface whenever it is implemented, such as List.sort()
          try {
            if (privateLookupInMethod == null) {
              return new DefaultMethodInvoker(getMethodHandleJava8(method));
            } else {
              return new DefaultMethodInvoker(getMethodHandleJava9(method));
            }
          } catch (IllegalAccessException | InstantiationException | InvocationTargetException
              | NoSuchMethodException e) {
            throw new RuntimeException(e);
          }
        } else {
          // Create a MapperMethod
          return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
        }
      });
    } catch (RuntimeException re) {
      Throwable cause = re.getCause();
      throw cause == null ? re : cause;
    }
  }

Default method judgments are made first, and then the invoke() method of PlainMethodInvoker is entered, which is also the entry point for true SQL execution.

@Override
    public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
      // The real starting point for SQL execution
      return mapperMethod.execute(sqlSession, args);
    }

2.mapperMethod.execute()

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) { // Call SqlSession's corresponding method based on the type of SQL statement
      case INSERT: {
        // Associate user-passed arguments with specified parameter names by processing args[] arrays with ParamNameResolver
        Object param = method.convertArgsToSqlCommandParam(args);
        // sqlSession.insert(command.getName(), param) calls the insert method of SqlSession
        // The rowCountResult method converts the results based on the return value type of the method recorded in the method field
        result = rowCountResult(sqlSession.insert(command.getName(), param));
        break;
      }
      case UPDATE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.update(command.getName(), param));
        break;
      }
      case DELETE: {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = rowCountResult(sqlSession.delete(command.getName(), param));
        break;
      }
      case SELECT:
        if (method.returnsVoid() && method.hasResultHandler()) {
          // Method with empty return value and ResultSet handled by ResultHandler
          executeWithResultHandler(sqlSession, args);
          result = null;
        } else if (method.returnsMany()) {
          result = executeForMany(sqlSession, args);
        } else if (method.returnsMap()) {
          result = executeForMap(sqlSession, args);
        } else if (method.returnsCursor()) {
          result = executeForCursor(sqlSession, args);
        } else {
          // Method that returns a single object
          Object param = method.convertArgsToSqlCommandParam(args);
          // Execution Entry of Common select Statement >>
          result = sqlSession.selectOne(command.getName(), param);
          if (method.returnsOptional()
              && (result == null || !method.getReturnType().equals(result.getClass()))) {
            result = Optional.ofNullable(result);
          }
        }
        break;
      case FLUSH:
        result = sqlSession.flushStatements();
        break;
      default:
        throw new BindingException("Unknown execution method for: " + command.getName());
    }
    if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
      throw new BindingException("Mapper method '" + command.getName()
          + " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
    }
    return result;
  }

Here, according to different types (INSERT, UPDATE, DELETE, SELECT) and return types:

  • Call the convertArgsToSqlCommandParam() method to convert the parameters to SQL parameters.
  • Call the insert(), update(), delete(), selectOne() methods of SqlSession.

Take selectOne() as an example.

3.sqlSession.selectOne()

@Override
  public <T> T selectOne(String statement, Object parameter) {
    // Going to DefaultSqlSession
    // Popular vote was to return null on 0 results and throw exception on too many.
    List<T> list = this.selectList(statement, parameter);
    if (list.size() == 1) {
      return list.get(0);
    } else if (list.size() > 1) {
      throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
    } else {
      return null;
    }
  }

selectOne() also calls the selectList() method, in which you first get the MappedStatement in Configuration based on the statement ID.Variable ms has all the attributes in xml that add or delete to the check label configuration, including id, statementType, sqlSource, useCache, and so on.

@Override
  public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      // If cacheEnabled = true (default), Executor will be decorated with CachingExecutor
      return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

Then executor.query()

Executor was created at openSeesion, created the basic type of executor, then wrapped in a secondary cache, followed by a plug-in wrapper.

If wrapped by a plug-in, you go to the logic of the plug-in, then to the logic of the CachingExecutor, and finally to the query() method of the BaseExecutor.

4.CachingExecutor.query()

  @Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // Get SQL
    BoundSql boundSql = ms.getBoundSql(parameterObject);
    // Create CacheKey: What kind of SQL is the same SQL?>>
    CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
    return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

What is the CacheKey of the secondary cache?The same CacheKey means that two queries are the same query.

Entering the Executor.createCacheKey() method, we can see the six elements that make up it:

@Override
  public CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql) {
    if (closed) {
      throw new ExecutorException("Executor was closed.");
    }
    CacheKey cacheKey = new CacheKey();
    //Element One
    cacheKey.update(ms.getId()); 
    //Element Two
    cacheKey.update(rowBounds.getOffset()); 
    //Element Three
    cacheKey.update(rowBounds.getLimit()); 
    //Element Four
    cacheKey.update(boundSql.getSql());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    TypeHandlerRegistry typeHandlerRegistry = ms.getConfiguration().getTypeHandlerRegistry();
    for (ParameterMapping parameterMapping : parameterMappings) {
      if (parameterMapping.getMode() != ParameterMode.OUT) {
        Object value;
        String propertyName = parameterMapping.getProperty();
        if (boundSql.hasAdditionalParameter(propertyName)) {
          value = boundSql.getAdditionalParameter(propertyName);
        } else if (parameterObject == null) {
          value = null;
        } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
          value = parameterObject;
        } else {
          MetaObject metaObject = configuration.newMetaObject(parameterObject);
          value = metaObject.getValue(propertyName);
        }
        //Element Five
        cacheKey.update(value); 
      }
    }
    if (configuration.getEnvironment() != null) {
      //Element Six
      cacheKey.update(configuration.getEnvironment().getId());
    }
    return cacheKey;
  }

From the six elements of CacheKey, we can see that the same query is considered to be the same query if the methods are the same, the page offsets are the same, the SQL statements are the same, the parameter values are the same, and the data source environment is the same.

Let's look at the properties of CacheKey:

	private static final int DEFAULT_MULTIPLIER = 37;
  private static final int DEFAULT_HASHCODE = 17;
  private final int multiplier;
  private int hashcode;
  private long checksum;
  private int count;
  private List<Object> updateList;

How do I compare two CacheKey values to be equal?It is inefficient to compare these six features six times if they are equal in turn.Mybatis calculates hash codes. Every class that inherits Object has a hashCode() method. When the CacheKey value is generated (update method), it also updates the hashCode of CacheKey, which is generated by a multiplication hash (cardinality baseHashCode=17, multiplication factor multiplier=37).

hashcode = multiplier * hashcode + baseHashCode;

When CacheKey is generated, the query() method is called.

5.BaseExecutor.query()

@Override
  public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
      throws SQLException {
    Cache cache = ms.getCache();
    // Where was the cache object created?XMLMapperBuilder class xmlconfigurationElement()
    // Determined by <cache>tag
    if (cache != null) {
      // flushCache="true" Empty Level 1 and Level 2 caches >>
      flushCacheIfRequired(ms);
      if (ms.isUseCache() && resultHandler == null) {
        ensureNoOutParams(ms, boundSql);
        // Get secondary cache
        // Cache is managed through TransactionalCache Manager, TransactionalCache
        @SuppressWarnings("unchecked")
        List<E> list = (List<E>) tcm.getObject(cache, key);
        if (list == null) {
          list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
          // Write to Level 2 Cache
          tcm.putObject(cache, key, list); // issue #578 and #116
        }
        return list;
      }
    }
    // Go to SimpleExecutor | ReuseExecutor | BatchExecutor
    return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
  }

@Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // ErrorContext for Exception Systems
    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()) {
      // When flushCache="true", even queries empty the first level cache
      clearLocalCache();
    }
    List<E> list;
    try {
      // Prevent recursive queries from repeating cache processing
      queryStack++;
      // Query Level 1 Cache
      // Differences between ResultHandler and ResultSetHandler
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        // True query process
        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;
  }

private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    // Preemptive Placement
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // The difference between the three Executor s, see doUpdate
      // Default Simple
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      // Remove placeholders
      localCache.removeObject(key);
    }
    // Write Level 1 Cache
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

6.SimpleExecutor.doQuery()

@Override
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
  Statement stmt = null;
  try {
    Configuration configuration = ms.getConfiguration();
    // Note that you have reached StatementHandler >>the key object for SQL processing
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    // Get a Statement object
    stmt = prepareStatement(handler, ms.getStatementLog());
    // Execute Query
    return handler.query(stmt, resultHandler);
  } finally {
    // Close when used up
    closeStatement(stmt);
  }
}

RoutingStatementHandler is derived from the configuration.newStatementHandler() method.The RoutingStatementHandler determines the type of StatementHandler based on the statementType inside the MappedStatement.The default is PREPARED.

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  // How did StatementType come from?Add or delete statementType="PREPARED" in check label, default value PREPARED
  switch (ms.getStatementType()) {
    case STATEMENT:
      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:
      // What did you do when you created the StatementHandler?>>
      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:
      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
      throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  }

}

The StatementHandler contains the ParameterHandler for processing parameters and the ResultSetHandler for processing result sets.

protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
  this.configuration = mappedStatement.getConfiguration();
  this.executor = executor;
  this.mappedStatement = mappedStatement;
  this.rowBounds = rowBounds;

  this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
  this.objectFactory = configuration.getObjectFactory();

  if (boundSql == null) { // issue #435, get the key before calculating the statement
    generateKeys(parameterObject);
    boundSql = mappedStatement.getBoundSql(parameterObject);
  }

  this.boundSql = boundSql;

  // Created the other two big objects of the four major objects >>
  // What were you doing when you created these two objects?
  this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
  this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);
}

These three objects are one of the four objects that plug-ins can intercept, so they are wrapped in interceptors after they are created.

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  // Implant plug-in logic (return proxy object)
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
      ResultHandler resultHandler, BoundSql boundSql) {
    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
    // Implant plug-in logic (return proxy object)
    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    return resultSetHandler;
  }

public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
    // Implant plug-in logic (return proxy object)
    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
    return statementHandler;
  }

Create a Statement object by creating a StatementHandler.

// Get a Statement object
stmt = prepareStatement(handler, ms.getStatementLog());
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
  Statement stmt;
  Connection connection = getConnection(statementLog);
  // Get Statement Object
  stmt = handler.prepare(connection, transaction.getTimeout());
  // Set parameters for Statement
  handler.parameterize(stmt);
  return stmt;
}

Perform queries and, if there is a plug-in wrapper, go to the blocked business first.

// Execute Query
return handler.query(stmt, resultHandler);

Processing into PreparedStatementHandler

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  // Process to JDBC
  ps.execute();
  // Processing result set
  return resultSetHandler.handleResultSets(ps);
}

Execute the execute() method of PreparedStatement followed by the execution of PreparedStatement in the JDBC package.

ResultSetHandler processes the result set, and if there is a plug-in wrapper, it is processed first in the intercepted business.

2. Summary

1.Mybatis Core Objects

objectRelated ObjectsEffect
ConfigurationMapperRegistry
TypeAlisaRegistry
TypeHandlerRegistry
Contains all configuration information for Mybaits
SqlSessionSqlSessionFactory
DefaultSqlSession
Encapsulate API s for add-delete change checks of operational databases and provide them to the application layer for use
ExecutorBaseExecutor
SimpleExecutor
BatchExecutor
ReuseExecutor
Mybatis executor, the core of Mybatis scheduling, is responsible for generating SQL statements and maintaining query cache
StatementHandlerBaseStatementHandler
SimpleStatementHandler
PreparedStatementHandler
CallableStatementHandler
Encapsulates Statement operations in JDBC
ParameterHandlerDefaultParameterHandlerConverts the passed parameter to the parameter required by Statement in JDBC
ResultSetHandlerDefaultResultSetHandlerConverts the ResultSet result set object returned by JDBC to a collection of List type
MapperProxyMapperProxyFactoryUsed to proxy Mapper interface methods
MappedStatementSqlSource
BoundSql
MappedStatement maintains the encapsulation of a <select|update|delete|insert>node that contains an SQL SQL SQL SQL information, entry and exit information

Keywords: Java Database Mybatis SQL source code

Added by feyd on Sun, 05 Sep 2021 23:40:38 +0300