The underlying query principle of MyBatis for source code learning

Guided reading

This article starts with a low version bug of MyBatis (version before 3.4.5), analyzes a complete query process of MyBatis, and interprets a query process of MyBatis in detail from the parsing of the configuration file to the complete execution process of a query. Learn more about the one-time query process of MyBatis. In the usual code writing, a bug in a low version of MyBatis (the version before 3.4.5) was found. Since the version in many projects is lower than 3.4.5, it is reproduced here with a simple example. problem, and analyze the process of MyBatis query from the perspective of source code, so that everyone can understand the query principle of MyBatis.

 

1 Problem phenomenon

1.1 Scenario problem reproduction

As shown in the figure below, in the example Mapper, a method queryStudents is provided below to query the data that meets the query conditions from the student table. The input parameter can be student_name or a collection of student_name. In the example, the parameter only passes in the List of studentName. gather

 List<String> studentNames = new LinkedList<>();
 studentNames.add("lct");
 studentNames.add("lct2");
 condition.setStudentNames(studentNames);

 

  <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">


        select * from student
        <where>
            <if test="studentNames != null and studentNames.size > 0 ">
                AND student_name IN
                <foreach collection="studentNames" item="studentName" open="(" separator="," close=")">
                    #{studentName, jdbcType=VARCHAR}
                </foreach>
            </if>


            <if test="studentName != null and studentName != '' ">
                AND student_name = #{studentName, jdbcType=VARCHAR}
            </if>
        </where>
    </select>

The expected result of running is

select * from student WHERE student_name IN ( 'lct' , 'lct2' )

But the actual result of running is

==> Preparing: select * from student WHERE student_name IN ( ? , ? ) AND student_name = ?

==> Parameters: lct(String), lct2(String), lct2(String)

<== Columns: id, student_name, age

<== Row: 2, lct2, 2

<== Total: 1

It can be seen from the running results that the student_name is not assigned a separate value, but after MyBatis parsing, a value is assigned to the student_name alone. It can be inferred that MyBatis has a problem in parsing SQL and assigning values ​​to variables. The initial guess is the foreach loop. The value of the variable in the foreach is brought to the outside of the foreach, resulting in an exception in the SQL parsing.

2 MyBatis query principle

2.1 MyBatis Architecture

2.1.1 Architecture Diagram

Let's take a brief look at the overall architecture model of MyBatis. On the whole, MyBatis is mainly divided into four modules:

Interface layer : the main function is to deal with the database

Data processing layer : The data processing layer can be said to be the core of MyBatis. It has two functions:

  • Build dynamic SQL statements by passing in parameters;
  • Execution of SQL statements and encapsulation of query results to integrate List<E>

Framework support layer : mainly includes transaction management, connection pool management, caching mechanism and configuration of SQL statements

Boot Layer : The boot layer is the way to configure and start MyBatis configuration information. MyBatis provides two ways to guide MyBatis: XML configuration file-based and Java API-based

2.1.2 Four MyBatis Objects

There are four core objects that run through the entire framework of MyBatis, ParameterHandler, ResultSetHandler, StatementHandler and Executor. The four objects run through the execution process of the entire framework. The main functions of the four objects are:

  • ParameterHandler: set precompiled parameters
  • ResultSetHandler: Handles the returned result set of SQL
  • StatementHandler: handles sql statement precompiling, setting parameters and other related work
  • Executor: The executor of MyBatis, used to perform addition, deletion, modification and query operations

2.2 Interpret a query process of MyBatis from the source code

First give the code to reproduce the problem and the corresponding preparation process

2.2.1 Data Preparation

CREATE TABLE `student`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `student_name` varchar(255) NULL DEFAULT NULL,
  `age` int(11) NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1;


-- ----------------------------
-- Records of student
-- ----------------------------
INSERT INTO `student` VALUES (1, 'lct', 1);
INSERT INTO `student` VALUES (2, 'lct2', 2);

 

2.2.2 Code preparation

1.mapper configuration file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >


<mapper namespace="mybatis.StudentDao">
    <!-- 映射关系 -->
    <resultMap id="resultMap" type="mybatis.Student">
        <id column="id" property="id" jdbcType="BIGINT" />
        <result column="student_name" property="studentName" jdbcType="VARCHAR" />
        <result column="age" property="age" jdbcType="INTEGER" />


    </resultMap>


    <select id="queryStudents" parameterType="mybatis.StudentCondition" resultMap="resultMap">


        select * from student
        <where>
            <if test="studentNames != null and studentNames.size > 0 ">
                AND student_name IN
                <foreach collection="studentNames" item="studentName" open="(" separator="," close=")">
                    #{studentName, jdbcType=VARCHAR}
                </foreach>
            </if>


            <if test="studentName != null and studentName != '' ">
                AND student_name = #{studentName, jdbcType=VARCHAR}
            </if>
        </where>
    </select>


</mapper>

2. Sample code

public static void main(String[] args) throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        //1.获取SqlSessionFactory对象
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //2.获取对象
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //3.获取接口的代理类对象
        StudentDao mapper = sqlSession.getMapper(StudentDao.class);
        StudentCondition condition = new StudentCondition();
        List<String> studentNames = new LinkedList<>();
        studentNames.add("lct");
        studentNames.add("lct2");
        condition.setStudentNames(studentNames);
        //执行方法
        List<Student> students = mapper.queryStudents(condition);
    }

 

2.2.3 Query Process Analysis

1. Construction of SqlSessionFactory

First look at the creation process of the SqlSessionFactory object

//1.获取SqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

The code first obtains the object by calling the build method in SqlSessionFactoryBuilder and enters the build method

 public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }

call its own build method

Figure 1 The build method itself calls debugging legend

In this method, an XMLConfigBuilder object will be created to parse the incoming MyBatis configuration file, and then the parse method will be called for parsing.

Figure 2 parse parsing input parameter debugging legend

In this method, the content of xml will be obtained from the root directory of the configuration file of MyBatis. The parser object is an XPathParser object, which is specially used to parse xml files. How to get each node from the xml file? No further explanation is given here. Here you can see that parsing the configuration file starts from the configuration node, which is also the root node in the MyBatis configuration file

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>


    <properties>
        <property name="dialect" value="MYSQL" />  <!-- SQL方言 -->
    </properties>

Then pass the parsed xml file into the parseConfiguration method, in which the configuration of each node in the configuration file will be obtained

Figure 3 Parsing configuration debugging legend

To get the configuration of the mappers node to see the specific parsing process

 <mappers>
        <mapper resource="mappers/StudentMapper.xml"/>
    </mappers>

Enter the mapperElement method

mapperElement(root.evalNode("mappers"));

Figure 4 mapperElement method debugging legend

See that MyBatis still parses the mappers node by creating an XMLMapperBuilder object, in the parse method

public void parse() {
  if (!configuration.isResourceLoaded(resource)) {
    configurationElement(parser.evalNode("/mapper"));
    configuration.addLoadedResource(resource);
    bindMapperForNamespace();
  }


  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}

Parse each mapper file configured by calling the configurationElement method

private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.equals("")) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
  }
}

Let's take a look at how to parse a mapper file by parsing the CRUD tags in mapper

Enter the buildStatementFromContext method

private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

It can be seen that MyBatis still parses the addition, deletion, modification and query nodes by creating an XMLStatementBuilder object. By calling the parseStatementNode method of this object, all configuration information configured under this tag will be obtained in this method, and then set.

Figure 5 parseStatementNode method debugging legend

After the parsing is completed, add all the configurations to a MappedStatement through the method addMappedStatement, and then add the mappedstatement to the configuration

builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
    fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
    resultSetTypeEnum, flushCache, useCache, resultOrdered, 
    keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);

 

You can see that a mappedstatement contains the details of a CRUD tag

Figure 7 Mappedstatement object method debugging legend

And a configuration contains all the configuration information, including mapperRegistertry and mappedStatements

Figure 8 Config object method debugging legend

specific process

Figure 9 The construction process of the SqlSessionFactory objectFigure 9 The construction process of the SqlSessionFactory object

2. SqlSession creation process

After the SqlSessionFactory is created, let's take a look at the creation process of the SqlSession

SqlSession sqlSession = sqlSessionFactory.openSession();

First, the openSessionFromDataSource method of DefaultSqlSessionFactory will be called

@Override
public SqlSession openSession() {
  return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

In this method, the properties such as DataSource are first obtained from the configuration to form the object Environment, and a transaction object TransactionFactory is constructed using the properties in the Environment

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();
  }
}

After the transaction is created, the Executor object is created. The creation of the Executor object is based on the executorType. The default is the SIMPLE type. If there is no configuration, the SimpleExecutor is created. If the second level cache is enabled, the CachingExecutor will be created.

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);
  }
  executor = (Executor) interceptorChain.pluginAll(executor);
  return executor;
}

After the executor is created, the executor = (Executor)
interceptorChain.pluginAll(executor) method will be executed. The corresponding meaning of this method is to use each interceptor to wrap and return the executor, and finally call the DefaultSqlSession method to create a SqlSession

Figure 10 The creation process of the SqlSession object

3. The acquisition process of Mapper

With SqlSessionFactory and SqlSession, you need to get the corresponding Mapper and execute the methods in the mapper

StudentDao mapper = sqlSession.getMapper(StudentDao.class);

In the first step, we know that all mappers are placed in the MapperRegistry object, so
get the corresponding mapper by calling the org.apache.ibatis.binding.MapperRegistry#getMapper method

public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
  final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
  if (mapperProxyFactory == null) {
    throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
  }
  try {
    return mapperProxyFactory.newInstance(sqlSession);
  } catch (Exception e) {
    throw new BindingException("Error getting mapper instance. Cause: " + e, e);
  }
}

In MyBatis, all mappers correspond to a proxy class. After obtaining the proxy class corresponding to the mapper, execute the newInstance method to obtain the corresponding instance, so that the method can be called through this instance.

public class MapperProxyFactory<T> {


  private final Class<T> mapperInterface;
  private final Map<Method, MapperMethod> methodCache = new ConcurrentHashMap<Method, MapperMethod>();


  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }


  public Class<T> getMapperInterface() {
    return mapperInterface;
  }


  public Map<Method, MapperMethod> getMethodCache() {
    return methodCache;
  }


  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }


  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }


}

The process of getting mapper is

Figure 11 The acquisition process of Mapper

4. Query process

After getting the mapper, you can call the specific method

//执行方法
List<Student> students = mapper.queryStudents(condition);

First
, the method of org.apache.ibatis.binding.MapperProxy#invoke will be called. In this method, org.apache.ibatis.binding.MapperMethod#execute will be called

public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {
   Object param = method.convertArgsToSqlCommandParam(args);
      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()) {
        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 {
        Object param = method.convertArgsToSqlCommandParam(args);
        result = sqlSession.selectOne(command.getName(), param);
      }
      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;
}

First, determine which method to execute according to the type of SQL, add, delete, modify and check. Here, the SELECT method is executed. In SELECT, which method to execute is determined according to the return value type of the method. It can be seen that there is no separate method for selectone in select, but selectList is used. method, get the data by calling the
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object) method

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    MappedStatement ms = configuration.getMappedStatement(statement);
    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();
  }
}

In selectList, first obtain MappedStatement from the configuration object, which contains the relevant information of Mapper in the statement, and then call the
org.apache.ibatis.executor.CachingExecutor#query() method

Figure 12 Debugging diagram of query() method

In this method, the SQL is first parsed and the SQL is spliced ​​according to the input parameters and the original SQL.

Figure 13 SQL splicing process code diagram

The SQL finally parsed by calling getBoundSql in MapperedStatement is

Figure 14 Diagram of the result of the SQL splicing process

Next, call
org.apache.ibatis.parsing.GenericTokenParser#parse to parse the parsed SQL

Figure 15 Diagram of SQL parsing process

The final analysis result is

Figure 16 Diagram of SQL parsing results

Finally, the doQuery method in SimpleExecutor will be called. In this method, the StatementHandler will be obtained, and then
the method org.apache.ibatis.executor.statement.PreparedStatementHandler#parameterize will be called to process parameters and SQL, and finally the execute method of the statement will be called to obtain the The result set, and then use the resultHandler to process the knot

Figure 17 Diagram of SQL processing results

The main process of the query is

 

Figure 18 Query flow processing diagram

5. Summary of the query process

The entire query process is summarized as follows

Figure 19 Query process abstraction

2.3 Scenario problem causes and solutions

2.3.1 Personal investigation

The place where this bug appears is that when binding SQL parameters, the location in the source code is

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

Since the SQL written is a dynamic binding parameter SQL, it will eventually go to
the method org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql

public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if (parameterMappings == null || parameterMappings.isEmpty()) {
    boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
  }


  // check for nested result maps in parameter mappings (issue #30)
  for (ParameterMapping pm : boundSql.getParameterMappings()) {
    String rmId = pm.getResultMapId();
    if (rmId != null) {
      ResultMap rm = configuration.getResultMap(rmId);
      if (rm != null) {
        hasNestedResultMaps |= rm.hasNestedResultMaps();
      }
    }
  }


  return boundSql;
}

In this method, the rootSqlNode.apply(context) method will be called. Since this tag is a foreach tag, the apply method will be called to
the method org.apache.ibatis.scripting.xmltags.ForEachSqlNode#apply

@Override
public boolean apply(DynamicContext context) {
  Map<String, Object> bindings = context.getBindings();
  final Iterable<?> iterable = evaluator.evaluateIterable(collectionExpression, bindings);
  if (!iterable.iterator().hasNext()) {
    return true;
  }
  boolean first = true;
  applyOpen(context);
  int i = 0;
  for (Object o : iterable) {
    DynamicContext oldContext = context;
    if (first) {
      context = new PrefixedContext(context, "");
    } else if (separator != null) {
      context = new PrefixedContext(context, separator);
    } else {
        context = new PrefixedContext(context, "");
    }
    int uniqueNumber = context.getUniqueNumber();
    // Issue #709 
    if (o instanceof Map.Entry) {
      @SuppressWarnings("unchecked") 
      Map.Entry<Object, Object> mapEntry = (Map.Entry<Object, Object>) o;
      applyIndex(context, mapEntry.getKey(), uniqueNumber);
      applyItem(context, mapEntry.getValue(), uniqueNumber);
    } else {
      applyIndex(context, i, uniqueNumber);
      applyItem(context, o, uniqueNumber);
    }
    contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber));
    if (first) {
      first = !((PrefixedContext) context).isPrefixApplied();
    }
    context = oldContext;
    i++;
  }
  applyClose(context);
  return true;
}

When the appItm method is called, the parameters are bound, and the variable problems of the parameters will exist in the parameter area of ​​bindings

private void applyItem(DynamicContext context, Object o, int i) {
  if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
  }
}

When binding parameters, when binding the foreach method, you can see that not only the two parameters in the foreach are bound in the bindings, but also an additional parameter name studentName->lct2, which means that the last parameter will also be Appears in the bindings parameter,

private void applyItem(DynamicContext context, Object o, int i) {
  if (item != null) {
    context.bind(item, o);
    context.bind(itemizeItem(item, i), o);
  }
}

 

Figure 20 Parameter binding process

final judgment

org.apache.ibatis.scripting.xmltags.IfSqlNode#apply

@Override
public boolean apply(DynamicContext context) {
  if (evaluator.evaluateBoolean(test, context.getBindings())) {
    contents.apply(context);
    return true;
  }
  return false;
}

It can be seen that when the evaluateBoolean method is called, the context.getBindings() is the bindings parameter mentioned above and passed in, because now there is a studentName in this parameter, so when using the Ognl expression, it is determined as this if tag is valuable, so this tag is parsed

Figure 21 Single parameter binding process

The final binding result is

Figure 22 All parameter binding process

Therefore, there is a problem with the binding parameter in this place, and the problem has been found so far.

2.3.2 Official explanation

Read the official documentation of MyBatis for verification, and found that there is such a sentence in the bug fixes in the release of version 3.4.5

Figure 23 This problem is officially fixed on github record Figure 23 This problem is officially fixed on github record

Fixed a bug in the modification of the global variable context in the foreach version

The issue address is https://github.com/mybatis/mybatis-3/pull/966

The fix is ​​https://github.com/mybatis/mybatis-3/pull/966/commits/84513f915a9dcb97fc1d602e0c06e11a1eef4d6a

 

You can see the official modification plan, redefining an object to store global variables and local variables respectively, which will solve the problem that foreach will change global variables.

Figure 24 An example of the official repair code for this problem

2.3.3 Repair plan

  • Upgrade MyBatis version to above 3.4.5
  • If the version remains unchanged, the variable name defined in foreach should not be consistent with the external one

3 Summary of the source code reading process

The directory of MyBatis source code is relatively clear, basically every module with the same function is together, but if you read the source code directly, it may still be difficult to understand its running process, this time through a simple The query process is followed from the beginning to the end, and you can see the design and processing flow of MyBatis, such as the design patterns used in it:

Figure 25 MyBatis code structure diagram

  • Combination mode: such as ChooseSqlNode, IfSqlNode, etc.
  • Template method pattern: such as BaseExecutor and SimpleExecutor, but also BaseTypeHandler and all subclasses such as IntegerTypeHandler
  • Builder mode: such as SqlSessionFactoryBuilder, XMLConfigBuilder, XMLMapperBuilder, XMLStatementBuilder, CacheBuilder
  • Factory mode: such as SqlSessionFactory, ObjectFactory, MapperProxyFactory
  • Proxy mode: the core of MyBatis implementation, such as MapperProxy, ConnectionLogger

4 Documentation Reference

https://mybatis.org/mybatis-3/en/index.htm

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/5581667