MyBatis, as an excellent persistence layer framework, has been used by more and more companies. Faced with the framework we use every day, how can we not read its source code? I spent a few days to read and debug MyBatis source code, now I share some of the understanding to everyone, if there are errors, but also hope to correct.

MyBatis source version: 3.5.8-snapshot, source address :github.com/mybatis/myb…

1. Introduction

The overall framework of MyBatis is roughly shown in the figure below, and each module can be singled out to write an article. Due to space, this article will only cover the whole process of [query] and will not go into too much detail. Examples include XML parsing, parameter mapping, caching, etc., which will be covered separately in a later article.This article only discusses MyBatis and analyzes the source code of MyBatis, so it will not integrate with Spring.

2. Sample program

[Requirement] Use MyBatis to complete a database query and query user records according to primary key ID.

1, write myBatis -config. XML configuration file.


      
<! DOCTYPEconfiguration
  PUBLIC "- / / mybatis.org//DTD Config / 3.0 / EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
  <! -- Environment Configuration -->
  <environments default="development">
    <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="JDBC: mysql: / / 127.0.0.1:3306 / test"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
      </dataSource>
    </environment>
  </environments>

  <! - Mapper configuration - >
  <mappers>
    <mapper resource="mappers/UserMapper.xml"/>
  </mappers>
</configuration>
Copy the code

Write the DO class corresponding to the user table.

public class User {
  private Long id;
  private String userName;
  private String pwd;
  private String nickName;
  private String phone;
  private LocalDateTime createTime;
  // Omit the Getter and Setter methods
}
Copy the code

3. Write UserMapper interface.

public interface UserMapper {

  // Query the user by ID
  User getById(@Param("id") Long id);
}
Copy the code

4. Write the userMapper.xml file.


      
<! DOCTYPEmapper PUBLIC "- / / mybatis.org//DTD Mapper / 3.0 / EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.apache.mapper.UserMapper">

  <select id="getById" useCache="true" flushCache="false" resultType="org.apache.domain.User">
    select
    	id,user_name as userName,pwd,nick_name as nickName,phone,create_time as createTime
    from user
    where id = #{id}
  </select>
</mapper>
Copy the code

5. Write test programs.

public class Demo {
  public static void main(String[] args) throws Exception {
    String resource = "mybatis-config.xml";
    // Read the configuration file to get the input stream
    InputStream inputStream = Resources.getResourceAsStream(resource);
    // Build a callback factory from the input stream
    SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    // Open a new reply
    SqlSession session = sessionFactory.openSession(true);
    // Get the UserMapper proxy class object
    UserMapper mapper = session.getMapper(UserMapper.class);
    // Query user ID=1 and output to console
    System.err.println(mapper.getById(1L)); }}Copy the code

At this point, the sample program is complete, run the sample program, the console will output the user ID=1 information. Based on this program, we step by step analysis of MyBatis is how to achieve a database query to Java Bean mapping only by an interface and XML file.

3. Source analysis

3.1 Building the SqlSessionFactory

SqlSessionFactory is MyBatis’s callback factory, it is responsible for opening a SqlSession, we can add, delete, modify and check the database operation.

SqlSessionFactory is an interface. The default implementation class is DefaultSqlSessionFactory.

Some code is posted here:

public interface SqlSessionFactory {

  // Open a new reply
  SqlSession openSession(a)

  /** * Open a new reply * @param autoCommit whether auto-commit * @return */
  SqlSession openSession(boolean autoCommit);

  /** * Open a new reply with the specified transaction isolation level *@paramLevel Transaction isolation level *@return* /
  SqlSession openSession(TransactionIsolationLevel level);
    
  // Get the global configuration
  Configuration getConfiguration(a);
}
Copy the code

SqlSessionFactory is built in builder mode. The corresponding class is SqlSessionFactoryBuilder. There are multiple overloads of the build() method, which you can build with a character stream Reader or an InputStream. It can also be built using the global Configuration object Configuration. In fact, either way, the final construction is through the Configuration object, the first two methods of building MyBatis is just from the character stream/byte stream config. XML Configuration file parsing into the Configuration object.

public class SqlSessionFactoryBuilder {
  // Build from a character stream
  public SqlSessionFactory build(Reader reader) {
    return build(reader, null.null);
  }
  
  // Build from a byte stream
  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null.null);
  }
  
  // Build by configuring objects, and ultimately build by this method
  public SqlSessionFactory build(Configuration config) {
    return newDefaultSqlSessionFactory(config); }}Copy the code

3.2 Configuration construction

Configuration is the global Configuration class of MyBatis. It can be regarded as the description object of MyBatis Configuration file in Java. This class is so important, and so large, that I won’t go into detail here; I’ll write about it later.

To build an SqlSessionFactory, you need a Configuration. In fact, there are two ways. One is to manually create the Configuration object, and the other is to write the XML file and let MyBatis parse it.

This is typically done in a configuration file, so we’ll focus on xmlconfigBuilder-parse ().

// Parse the Configuration from the XML Configuration file
public Configuration parse(a) {
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // Start parsing from the root node 
      
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
}

// Parse from the root node
private void parseConfiguration(XNode root) {
  try {
    /** ** ** ** ** ** ** Parse the Settings TAB and read the Settings item * 3. Parse the alias of the class * 4. Parse plug-in configuration * 5. Parse object factory configuration * 6. Parse object packaging factory configuration * 7. Parse runtime environment, multi-data source configuration * 8. Parse databaseIdProvider, multi-database support * 9. Parse typeHandlers, typeHandlers * 10. Parse mappers, register the Mapper interface */
    propertiesElement(root.evalNode("properties"));
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    loadCustomVfs(settings);
    loadCustomLogImpl(settings);
    typeAliasesElement(root.evalNode("typeAliases"));
    pluginElement(root.evalNode("plugins"));
    objectFactoryElement(root.evalNode("objectFactory"));
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    settingsElement(settings);
    environmentsElement(root.evalNode("environments"));
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    typeHandlerElement(root.evalNode("typeHandlers"));
    mapperElement(root.evalNode("mappers"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: "+ e, e); }}Copy the code

3.3 SqlSession

The Configuration file is parsed to generate the Configuration object. With the Configuration object, you can build the SqlSessionFactory. With the SqlSessionFactory, you can obtain the SqlSession object.

SqlSession is one of the most important interface of MyBatis, it is the only interface provided by MyBatis for developers to operate the database, greatly simplifying the operation of the database.

Let’s look at the interface definition so we know what it’s capable of. I’ll just post some code.

public interface SqlSession extends Closeable {
    
    // Query a record
    <T> T selectOne(String statement);
    
    // Query multiple records
    <E> List<E> selectList(String statement);
    
    / / query Map
    <K, V> Map<K, V> selectMap(String statement, String mapKey);
    
    // Cursor query
    <T> Cursor<T> selectCursor(String statement);
    
    // Insert data
    int insert(String statement);
    
    // Modify the data
    int update(String statement);
    
    // Delete the data
    int delete(String statement);
    
    // Commit the transaction
    void commit(a);
    
    // Roll back the transaction
    void rollback(a);
    
    // Get the proxy class for the Mapper interface
    <T> T getMapper(Class<T> type);
    
    // Obtain the database connection associated with the SqlSession
    Connection getConnection(a);
}
Copy the code

We can see through the interface, as long as there is a SqlSession object, we can do the database [add, delete, change and check] and the transaction operation, and it will automatically help us out of the result set and Java object mapping, very convenient.

3.4 Generating Mapper Proxy Objects

In general, we rarely operate the database directly through SqlSession. Instead, we will create an interface and operate through the proxy object generated by MyBatis, so we focus on getMapper() method.

The default implementation class of SqlSession is DefaultSqlSession, so just look at that.

/** * Obtain Mapper proxy object :MapperProxy *@see MapperProxy
 * @paramType Mapper Interface class *@param <T>
 * @return* /
@Override
public <T> T getMapper(Class<T> type) {
  return configuration.getMapper(type, this);
}
Copy the code

When we use SqlSession to get the Mapper proxy object, it is passed to the Configuration object for completion. Because when MyBatis parses the configuration file, it parses the mapper. XML file together and registers the parsing result in the MapperRegistry. MapperRegistry is a registry for the Mapper interface. It places the Mapper interface Class and the MapperProxyFactory into a Map container, where you can register the Mapper interface and get the proxy object for the Mapper interface.

public class MapperRegistry {
  // Global configuration
  private final Configuration config;
  // Mapping between the Mapper interface and MapperProxyFactory
  private finalMap<Class<? >, MapperProxyFactory<? >> knownMappers =new HashMap<>();

  // Get the proxy object for the Mapper interface
  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); }}// Register the Mapper interface
  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        knownMappers.put(type, new MapperProxyFactory<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        parser.parse();
        loadCompleted = true;
      } finally {
        if(! loadCompleted) { knownMappers.remove(type); } } } } }Copy the code

When you register a Mapper interface with MapperRegistry, it encapsulates the Mapper interface as a MapperProxyFactory because you rely on it to create a proxy object for the Mapper interface. The logic of creating a proxy object is also simple, because Mapper is an interface, so you can directly use JDK dynamic proxy, the MapperProxy of the generated proxy object.

/** * Mapper interface proxy factory * Function: generate Mapper interface proxy object *@param <T>
 */
public class MapperProxyFactory<T> {

  // Mapper Interface class
  private final Class<T> mapperInterface;
  // Method cache. When a Method is called through a proxy object, it first resolves the Method into MapperMethodInvoker
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

  @SuppressWarnings("unchecked")
  // Create a new instance and use JDK dynamic proxy to create proxy objects
  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<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
  // Omit part of the code...
}
Copy the code

3.5 MapperProxy

UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.getById(1L);
Copy the code

UserMapper is an interface, and interfaces can’t be instantiated, so where do mapper objects come from? When we call the first line of code, MyBatis actually generates a proxy object, MapperProxy, for us through the JDK dynamic proxy. When we call the second line of code, we are actually executing the mapperProxy.invoke () method.

If you invoke an Object method (hashCode, equals, etc.), you can invoke it directly from the proxy Object itself. You do not need to operate on the database. Otherwise, generate a MapperMethodInvoker object for it and call its invoke method.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      // If the method is inherited from the Object class, it can be called directly through the proxy Object itself
      return method.invoke(this, args);
    } else {
      MapperMethodInvoker 2; /** select * from cache where MapperMethodInvoker 2 is used. Execute the default, directly see: org. Apache. Ibatis. Binding. MapperProxy. PlainMethodInvoker. Invoke ()@see MapperMethod#execute(org.apache.ibatis.session.SqlSession, java.lang.Object[])
       */
      returncachedInvoker(method).invoke(proxy, method, args, sqlSession); }}catch (Throwable t) {
    throwExceptionUtil.unwrapThrowable(t); }}Copy the code

MapperMethodInvoker is MyBatis’ wrapper class for methods in the Mapper interface. It has two implementations: DefaultMethodInvoker and PlainMethodInvoker.

DefaultMethodInvoker is a wrapper around the default method in the Mapper interface. It also doesn’t need to operate on the database.

A PlainMethodInvoker object is generated for a user defined non-default Method, such as getById(), which needs to operate on the database.

// Obtain the MapperMethodInvoker object from the cache
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
      if (m.isDefault()) {
        // if you call the default method in the Interface, find the MethodHandle MethodHandle and call it through reflection.
        // That's not the point.
        try {
          if (privateLookupInMethod == null) {
            return new DefaultMethodInvoker(getMethodHandleJava8(method));
          } else {
            return newDefaultMethodInvoker(getMethodHandleJava9(method)); }}catch (IllegalAccessException | InstantiationException | InvocationTargetException
            | NoSuchMethodException e) {
          throw newRuntimeException(e); }}else {
        // The non-default method, which needs to operate on the database, generates PlainMethodInvoker object.
        // It will use the sqlSession to manipulate the database.
        return new PlainMethodInvoker(newMapperMethod(mapperInterface, method, sqlSession.getConfiguration())); }}); }catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null? re : cause; }}Copy the code

PlainMethodInvoker requires a MapperMethod object. For methods that need to operate on the database, it passes to MapperMethod to complete, which calls the mapperMethod.execute () method.

3.6 MapperMethod

MapperMethod is a wrapper class for MyBatis for Mapper methods that need to operate on a database.

/** * Encapsulation class of the SQL command to be executed * name: fully qualified name of the interface + method name to locate the unique SQL * type: type of the SQL command (add, delete, modify and query) */
private final SqlCommand command;
/** * method signature describes the return value of class * method? Returns a single object or a collection? * Whether to return Map? * and so on... * /
private final MethodSignature method;
Copy the code

SqlCommand is a description of the SQL command to be executed by the method. It records the Statement that uniquely identifies the SQL to be executed and the type of the SQL command.

public static class SqlCommand {
    // Interface fully qualified name + method name to locate the unique SQL to execute
    private final String name;
    // SQL command type: add, delete, modify, and so on
    private final SqlCommandType type;
}
Copy the code

MethodSignature is a description of a MethodSignature that records the method’s parameters, return values, paging, ResultHandler, and so on.

public static class MethodSignature {

    // Return multiple objects? Such as the List
    private final boolean returnsMany;
    // Return Map?
    private final boolean returnsMap;
    // Return Void?
    private final boolean returnsVoid;
    // Whether to return Cursor object?
    private final boolean returnsCursor;
    // Whether to return the Optional object?
    private final boolean returnsOptional;
    // Return type Class
    private finalClass<? > returnType;// Parse the value of the @mapKey annotation on the method
    private final String mapKey;
    // The subscript of the parameter list where the ResultHandler is located
    private final Integer resultHandlerIndex;
    // The subscript of the argument list where RowBounds reside (limits the number of results returned)
    private final Integer rowBoundsIndex;
    // A parameter name parser, which can be used directly in XML with @param annotation
    private final ParamNameResolver paramNameResolver;
    // Omit part of the code... .
}
Copy the code

When we execute the Mapper interface to operate the database, the MapperProxy object will execute the mapperMethod.execute () method, here will complete the database [add, delete, change and check] operation, need to pay attention to.

Space reason, here only look at the query. When userMapper.getByid (1L) is called, it first determines whether the method returns void and contains a ResultHandler as an argument. If so, it does not return the result, handing the query result to the ResultHandler to process. And then it says do I return multiple results? Return Map? Return cursor? Obviously, none of this is enough, so I’m just going to put in the last else.

To execute a normal query, it first needs to resolve the method parameters to ParamMap, which is essentially a HashMap. The Key is the parameter Name and the Value is the parameter Value. The purpose of parameter parsing is that you can use the parameters in XML using #{id}/${id} to implement dynamic SQL.

Once the argument is resolved, it calls sqlsession.selectOne () to query for a single result. See, we still use the SqlSession to manipulate the database, but we rarely use it directly, we use proxy objects.

After the query is complete, it checks whether the return type is Optional. If it is, it wraps the result with Optional and returns the result.

/** * execute Mapper method: execute SQL *@param sqlSession
 * @param args
 * @return* /
public Object execute(SqlSession sqlSession, Object[] args) {
  Object result;
  switch (command.getType()) {
    case INSERT: {// Insert operation
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.insert(command.getName(), param));
      break;
    }
    case UPDATE: {// Update operation
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.update(command.getName(), param));
      break;
    }
    case DELETE: {// Delete
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:// Query operation
      if (method.returnsVoid() && method.hasResultHandler()) {
        /** return Void (ResultHandler); /** return Void (ResultHandler)@see ResultHandler
         */
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        // Return multiple results
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        / / returns a Map
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        // Return Cursor
        result = executeForCursor(sqlSession, args);
      } else {
        /* Convert method arguments to parameter mappings that SQL statements use, so you can use the arguments in XML using #{param}. Generally, a Map structure is returned, such as "id":1, "param1":1 */
        Object param = method.convertArgsToSqlCommandParam(args);
        /* Run an SQL query. 1. Locate MappedStatement according to statement 2. 5. ResultSet processing, the ResultSet is converted to Java beans */
        result = sqlSession.selectOne(command.getName(), param);
        // If the result is Optional, the result is automatically wrapped.
        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;
}
Copy the code

3.7 ParamNameResolver

ParamNameResolver is MyBatis 】 【 parameter name the parser, it will process parameter, and added a @ Param parameter annotation, convert them to ParamMap.

First look at the properties:

public class ParamNameResolver {

  // The generated Name prefix
  public static final String GENERIC_NAME_PREFIX = "param";

  Arg0,arg1, arg0,arg1
  private final boolean useActualParamName;
    
  // Parameter subscript - The name of the corresponding parameter
  private final SortedMap<Integer, String> names;

  // Whether the @param annotation exists
  private boolean hasParamAnnotation;
}
Copy the code

When a MethodSignature object is created, the corresponding ParamNameResolver is created.

ParamNameResolver parses the parameter names and their subscripts in the constructor and places them in names. The parsing logic is: take the Name specified by @param annotation first, if there is no annotation, determine whether to take the actual parameter Name, otherwise take the parameter subscript as the Name.

Java reflection can obtain parameter names only if the version is JDK8 and the -parameters parameter is added at compile time. Otherwise, it will get meaningless parameter names such as arg0 and arg1.

/** * 1@ParamThe value of the annotation * 2. Obtain the parameter name * by reflection. 3
public ParamNameResolver(Configuration config, Method method) {
  this.useActualParamName = config.isUseActualParamName();
  finalClass<? >[] paramTypes = method.getParameterTypes();final Annotation[][] paramAnnotations = method.getParameterAnnotations();
  final SortedMap<Integer, String> map = new TreeMap<>();
  int paramCount = paramAnnotations.length;
  // get names from @Param annotations
  for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
    if (isSpecialParameter(paramTypes[paramIndex])) {
      // Skip the RowBounds and ResultHandler arguments
      continue;
    }
    String name = null;
    for (Annotation annotation : paramAnnotations[paramIndex]) {
      if (annotation instanceof Param) {
        // If @param is added, take the value of the annotation
        hasParamAnnotation = true;
        name = ((Param) annotation).value();
        break; }}if (name == null) {
      // @Param was not specified.
      if (useActualParamName) {
        // If the actual parameter name is used, the parameter name is obtained by reflection.
        // the JDK8 compiler class with -parameters can keep the parameter name, otherwise it will get meaningless parameter names such as arg0 and arg1
        name = getActualParamName(method, paramIndex);
      }
      if (name == null) {
        // If the name is null, use subscripts to get the parameters :#{param1},#{param2}...
        name = String.valueOf(map.size());
      }
    }
    map.put(paramIndex, name);
  }
  // Make it immutable
  names = Collections.unmodifiableSortedMap(map);
}
Copy the code

The constructor is only responsible for parsing the parameter name corresponding to the parameter subscript. To get the parameter value corresponding to the parameter name, you need to call the getNamedParams() method. MyBatis provides an additional deterministic solution because the developer may not annotate and reflection may not get parameter names. In addition to using the known parameter name, it will automatically generate records based on the parameter subscript as #{param subscript} when parsing the parameter.

For example, the parameter ID in the example program will be resolved as follows:

"id" 	 > 1
"param1" > 1
Copy the code

The code is as follows:

/** * Obtain the parameter value (*) corresponding to the parameter name. * is usually a Map structure. If there is only one parameter, it is returned directly@param args
 * @return* /
public Object getNamedParams(Object[] args) {
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    // No parameters
    return null;
  } else if(! hasParamAnnotation && paramCount ==1) {
    // There is no @param annotation and there is only one parameter
    Object value = args[names.firstKey()];
    return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
  } else {
    final Map<String, Object> param = new ParamMap<>();
    int i = 0;
    for (Map.Entry<Integer, String> entry : names.entrySet()) {
      param.put(entry.getValue(), args[entry.getKey()]);
      final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
      // Generate param1,param2... You can also use #{param1} in XML
      if(! names.containsValue(genericParamName)) { param.put(genericParamName, args[entry.getKey()]); } i++; }returnparam; }}Copy the code

3.8 selectOne

After parameter resolution is complete, it is back to the query operation of SqlSession. In order to operate a database, you must first obtain a database Connection.

DefaultSqlSession is the default implementation class of SqlSession. Let’s see how it is created.

When we open a new reply from SqlSessionFactory, invoked openSessionFromDataSource () method. It first gets the runtime environment from the configuration object, and then gets the TransactionFactory from the environment. The TransactionFactory corresponds to the following configuration item in the configuration file, which is created when the configuration file is parsed, typically JdbcTransactionFactory.

<transactionManager type="JDBC"/>
Copy the code

JdbcTransactionFactory opens a new transaction, JdbcTransaction, which contains the DataSource and the database Connection.

public class JdbcTransaction implements Transaction {
  // Database connection
  protected Connection connection;
  / / the data source
  protected DataSource dataSource;
  Transaction isolation level
  protected TransactionIsolationLevel level;
  // Whether to submit automatically
  protected boolean autoCommit;
}
Copy the code

With JdbcTransaction, we can create an Executor according to the ExecutorType. Executor is the executable interface for MyBatis to operate the database. Use DefaultSqlSession to create an Executor session.

/** * Open a Session from the DataSource *@paramExecType Type of the actuator: simple, reusable, batch *@paramLevel Transaction isolation level *@paramAutoCommit Whether to automatically commit *@return* /
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // Get the runtime environment: created when parsing XML
    final Environment environment = configuration.getEnvironment();
    // Get the transaction factory associated with the Environment
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // Use the factory class to create a transaction manager, typically JdbcTransaction
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // Create an executor based on the transaction manager and executor type, typically SimpleExecutor
    final Executor executor = configuration.newExecutor(tx, execType);
    // Create a SqlSession
    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

Now that you have the SqlSession, you can manipulate the databaseselectOne()Method, it’s actually going to executeselectList(), but will automatically take the 0 result, so we focus onselectList().

The statement is the unique identifier of the SQL to be executed, consisting of [fully qualified interface name + method name], so MyBatis does not support overloaded interface methods. The statement is used to locate a single SQL node in the XML, which is represented in Java by the MappedStatement class.

The MappedStatement class is also important, which will be explained in detail in the next section. For now, all you need to know is that it represents the SQL tag node, and that you know what SQL statement to execute and what results to return.

After the MappedStatement is located, it is time to call executor.query () to perform the query.

/** * Query multiple results *@paramStatement Uniquely identifies the SQL to be executed: fully qualified name of the interface + method name *@paramParameter Parameter list *@paramRowBounds Paging conditions *@paramHandler specifies the ResultHandler, which defaults to null * if there is no ResultHandler in the argument@param <E>
 * @return* /
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // Retrieve the MappedStatement(created when parsing the SQL tag in the XML) from the Map cache.
    MappedStatement ms = configuration.getMappedStatement(statement);
    return 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

3.9 MappedStatement

MappedStatement can be interpreted as MyBatis’ description of [add, Delete, modify and Check] SQL node in XML file. MyBatis will automatically generate this object when parsing XML file. Detailed code in XMLStatementBuilder. ParseStatementNode (), there is no detail.

The MappedStatement object records which XML file the SQL node came from, what the SQL statement is, what type of result is returned, and so on. This class is also important because it allows you to know what SQL statements to execute and how to data map the result set.

Class size, space reasons, only for attribute description:

public final class MappedStatement {

  // From which XML file
  private String resource;
  // Global configuration
  private Configuration configuration;
  // Unique identifier: fully qualified interface name + method name
  private String id;
  // Limit the maximum number of rows returned by SQL execution
  private Integer fetchSize;
  // The timeout period for SQL execution
  private Integer timeout;
  // The Statement type used to execute the SQL
  private StatementType statementType;
  // The type of the result set returned
  private ResultSetType resultSetType;
  // The source of the executed SQL statement from which to obtain the executed SQL statement
  private SqlSource sqlSource;
  // Level 2 cache
  private Cache cache;
  // Encapsulate the parameterMap attribute in the tag
  private ParameterMap parameterMap;
  // 
      
        Encapsulates the tag and configures the mapping relationship between database fields and Java class attributes
      
  private List<ResultMap> resultMaps;
  // Whether to refresh the cache
  private boolean flushCacheRequired;
  // Whether to use caching
  private boolean useCache;
  private boolean resultOrdered;
  // Type of SQL command
  private SqlCommandType sqlCommandType;
  // Primary key generator, which returns primary keys when insert data
  private KeyGenerator keyGenerator;
  // Attributes to which the primary key is applied
  private String[] keyProperties;
  // The column where the primary key is applied
  private String[] keyColumns;
  // Whether there are nested results
  private boolean hasNestedResultMaps;
  // Data source ID to distinguish between multiple data sources
  private String databaseId;
  / / log
  private Log statementLog;
  // Different language driven
  private LanguageDriver lang;
  // When multiple result sets are returned
  private String[] resultSets;
}
Copy the code

3.10 Executor

The Executor interface is provided by MyBatis to operate the database. SqlSession operates the database by delegating Executor to execute.

Post some of the Executor definitions to see the capabilities it has, mainly for database manipulation, transaction management, cache management, and so on.

public interface Executor {

  /** * Execute SQL query *@paramThe Statement executed by ms is: which SQL tag node of which XML is executed? *@paramThe parameter parameters *@paramRowBounds Paging data *@paramResultHandler resultHandler *@param <E>
   * @return
   * @throws SQLException
   */
  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  // Cursor query
  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  // Batch execution
  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 cache key for level 1 cache
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

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

  // Clear the local cache
  void clearLocalCache(a);
  // Omit part of the code......
}
Copy the code

Executor has an abstract subclass, BaseExecutor, which is the parent class of the other implementation classes and uses the template method to implement the basic functionality.

Subclasses are as follows:

The implementation class instructions
SimpleExecutor A simple executor that creates a new Statement each time it is executed
ReuseExecutor Reuse the executor and cache the same SQL Statement to avoid frequent Statement creation and optimize performance
BatchExecutor Batch operation actuators
CachingExecutor Support for level 2 caching of the actuator, decorator pattern

Caching is enabled by default, so let’s look directly at cachingExecutor.query (). It first calls ms.getboundSQL () to parse the SQL. This step completes the dynamic stitching of the SQL and completes the replacement of the ${}/#{} parameter. Then create CacheKey, which is a level 1 cache based on SqlSession. It is created according to the rule of [StatementID+SQL+ parameter + paging]. The data will only hit the cache if it is identical. Then it’s time to call query().

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  ${} = ${} = ${} = ${
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  //System.err.println("###SQL:" + boundSql.getSql());
  // Create the cache key based on the executed [StatementID+SQL+ parameter + paging]. Only these data are all the same to hit the cache.
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Copy the code

The CachingExecutor function relies on an Executor internally. The CachingExecutor function itself does not handle database operations. The CachingExecutor function is to determine whether a query has hit the level-2 cache. The results are then cached. Here is the Query () method overridden by CachingExecutor.

@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) { // Mapper Indicates whether the level-2 cache is enabled
    // Determine whether the cache needs to be cleared
    flushCacheIfRequired(ms);
    // Whether the Statement is cached
    if (ms.isUseCache() && resultHandler == null) {
      // If the stored procedure is called, the second-level cache does not support saving parameters of the output type, and will throw an exception
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // Get the data from the cache
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // If no, query the database and cache the result
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      returnlist; }}// Delegate to query
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
Copy the code

If the level 2 cache is not hit, baseExecutor.query () is called for the query. BaseExecutor first determines if the level-1 cache was hit, and then actually queries the database if it’s dead.

@Override
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
  // Report to ErrorContext that you are doing a query
  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()) {
    // If you need to flush the cache, clear the local cache
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    // Attempts to retrieve data from the level-1 cache
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if(list ! =null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // Query the databaselist = 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 #482clearLocalCache(); }}return list;
}
Copy the code

In the case that no cache is hit, doQuery() is called to actually query the database. Let’s look directly at SimpleExecutor.doQuery(). It will create StatementHandler first, and then prepared JDBC native Statement, finally call the JDBC native Statement. The execute () to execute SQL, and then to map the result set, the final results are obtained.

@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();
  /* Create a RoutingStatementHandler. The decorator pattern creates [Simple/Prepared/Callable]StatementHandler */ based on StatementType
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
    /** Prepare Statement 1. Create Statement 2. Set Timeout and FetchSize. 3. Set */
    stmt = prepareStatement(handler, ms.getStatementLog());
    // Execute the query and complete the result set mapping
    return handler.query(stmt, resultHandler);
  } finally{ closeStatement(stmt); }}Copy the code

3.11 StatementHandler

StatementHandler is the processor description of a Statement in MyBatis. When the Executor to execute SQL, through the Configuration. The newStatementHandler () to create StatementHandler, RoutingStatementHandler is created by default.

RoutingStatementHandler uses the delegate pattern. It doesn’t do any work itself. It just creates a corresponding StatementHandler based on the StatementType and then delegates it to do the work.

public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {

  // Create a delegate object based on StatementType
  switch (ms.getStatementType()) {
    case STATEMENT:/ / the ordinary
      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:// Precompiled, with support for setting parameters
      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:// Supports calling stored procedures
      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
      throw new ExecutorException("Unknown statement type: "+ ms.getStatementType()); }}Copy the code

There are three instances of the StatementType enumeration, which correspond to the three JDBC statements, as follows:

public enum StatementType {
  STATEMENT, // Statement Plain SQL, cannot set parameters
  PREPARED,  PreparedStatement PreparedStatement PreparedStatement PreparedStatement
  CALLABLE   // CallableStatement supports calling stored procedures
}
Copy the code

The mapping between the StatementHandler and JDBCStatement is as follows:

StatementHandler JDBC Statement
SimpleStatementHandler Statement
PreparedStatementHandler PreparedStatement
CallableStatementHandler CallableStatement

Let’s look at the process of preparing a Statement: first call subclass instantiateStatement() to create the Statement, then set Timeout and FetchSize for the Statement.

@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
  ErrorContext.instance().sql(boundSql.getSql());
  Statement statement = null;
  try {
    / * according to the Statement of StatementType create JDBC native SimpleStatementHandler > connection. The createStatement () PreparedStatementHandler > connection.prepareStatement() CallableStatementHandler > connection.prepareCall() */
    statement = instantiateStatement(connection);
    // Set the timeout period
    setStatementTimeout(statement, transactionTimeout);
    // Set the FetchSize, if XML is not configured
    setFetchSize(statement);
    return statement;
  } catch (SQLException e) {
    closeStatement(statement);
    throw e;
  } catch (Exception e) {
    closeStatement(statement);
    throw new ExecutorException("Error preparing statement. Cause: "+ e, e); }}Copy the code

PreparedStatementHandler: PreparedStatementHandler: PreparedStatementHandler: PreparedStatementHandler: PreparedStatementHandler: PreparedStatementHandler PreparedStatementHandler creates a PreparedStatement.

@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
        String[] keyColumnNames = mappedStatement.getKeyColumns();
        if (keyColumnNames == null) {
            return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
        } else {
            returnconnection.prepareStatement(sql, keyColumnNames); }}else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
        return connection.prepareStatement(sql);
    } else {
        returnconnection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY); }}Copy the code

PreparedStatement created, the next is to set parameters, the code in DefaultParameterHandler. SetParameters (). It will find the corresponding TypeHandler according to the parameter type. TypeHandler is the TypeHandler interface provided by MyBatis. It is used to set parameters for a Statement and to obtain results from a ResultSet for type conversion.

@Override
public void setParameters(PreparedStatement ps) {
  // Report to ErrorContext that you are setting parameters
  ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
  // Get the parameter mapping
  List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
  if(parameterMappings ! =null) {
    for (int i = 0; i < parameterMappings.size(); i++) {
      ParameterMapping parameterMapping = parameterMappings.get(i);
      if(parameterMapping.getMode() ! = ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty();if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
          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);
        }
        // Get TypeHandler. Our argument is Long, so it is LongTypeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          /* MyBatis can infer the JdbcType from the Java type when the parameter is not empty. If the parameter is NULL, no inferences can be made and the default type is used :JdbcType.OTHER In Oracle databases, NULL is passed to JdbcType
          jdbcType = configuration.getJdbcTypeForNull();
        }
        try {
          // Set parameters
          typeHandler.setParameter(ps, i + 1, value, jdbcType);
        } catch (TypeException | SQLException e) {
          throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
        }
      }
    }
  }
}
Copy the code

Our argument is a Long ID, so it corresponds to a LongTypeHandler, and look at how it sets the argument, which is pretty simple.

@Override
public void setNonNullParameter(PreparedStatement ps, int i, Long parameter, JdbcType jdbcType)
    throws SQLException {
    // Set the value of Long for the i-th parameter
    ps.setLong(i, parameter);
}
Copy the code

When the PreparedStatement is created and parameters are set, it is ready to execute. After executing, you get the ResultSet, and all that’s left is the mapping of the ResultSet, transforming the ResultSet into a Java Bean.

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    / / the Statement execution
    ps.execute();
    // Result set processing
    return resultSetHandler.handleResultSets(ps);
}
Copy the code

3.12 ResultSetHandler

�ResultSetHandler is a ResultSet handler provided by MyBatis. It is responsible for transforming the ResultSet returned by the database into the desired Java Bean.

The default implementation class is DefaultResultSetHandler, which first wraps the JDBC native ResultSet into MyBatis ResultSetWrapper and then calls handleResultSet() to process the ResultSet. A single ResultSet is converted into a JavaBean and stored in a list, which is then processed in a loop to return the list result.

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
  // Report to ErrorContext that you are processing the result set
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  // There may be multiple result sets, so use List to store them
  final List<Object> multipleResults = new ArrayList<>();

  int resultSetCount = 0;
  // If the result set exists, wrap it as MyBatis's ResultSetWrapper object
  ResultSetWrapper rsw = getFirstResultSet(stmt);

  // 
      
        Tag set corresponding to the resultMap attribute configured by the tag
      
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  int resultMapCount = resultMaps.size();
  // Check the quantity
  validateResultMapsCount(rsw, resultMapCount);
  / / processing
  while(rsw ! =null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    // Process the result set and get the multipleResults of the Java object set
    handleResultSet(rsw, resultMap, multipleResults, null);
    // Get the next result and continue the loop
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

  // handle the resultSets attribute, multiple resultSets
  String[] resultSets = mappedStatement.getResultSets();
  if(resultSets ! =null) {
    while(rsw ! =null && resultSetCount < resultSets.length) {
      ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
      if(parentMapping ! =null) {
        String nestedResultMapId = parentMapping.getNestedResultMapId();
        ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
        handleResultSet(rsw, resultMap, null, parentMapping); } rsw = getNextResultSet(stmt); cleanUpAfterHandlingResultSet(); resultSetCount++; }}Return multipleResults for the 0th element if there is only one result set. Otherwise, return multipleResults directly
  return collapseSingleResultList(multipleResults);
}
Copy the code

Why wrap ResultSet as ResultSetWrapper? To facilitate result mapping and type conversion. ResultSetWrapper records all columns returned by the result set, the corresponding Java and JDBC types of the columns, and the corresponding TypeHandler of the columns, which is required to perform the type conversion of the result set. Space reason, here paste part of the code:

public class ResultSetWrapper {

  // JDBC native result set
  private final ResultSet resultSet;
  // The TypeHandler registry
  private final TypeHandlerRegistry typeHandlerRegistry;
  / / column name
  private final List<String> columnNames = new ArrayList<>();
  // Name of the corresponding Class
  private final List<String> classNames = new ArrayList<>();
  // The corresponding JdbcType
  private final List<JdbcType> jdbcTypes = new ArrayList<>();
  private finalMap<String, Map<Class<? >, TypeHandler<? >>> typeHandlerMap =new HashMap<>();
  private final Map<String, List<String>> mappedColumnNamesMap = new HashMap<>();
  private final Map<String, List<String>> unMappedColumnNamesMap = new HashMap<>();

  public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
    super(a);this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.resultSet = rs;
    // Retrieve the result set metadata
    final ResultSetMetaData metaData = rs.getMetaData();
    // Number of columns in the result set
    final int columnCount = metaData.getColumnCount();
    // Parse all columns to get the column name, JdbcType, and corresponding Java class
    for (int i = 1; i <= columnCount; i++) { columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i)); jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i))); classNames.add(metaData.getColumnClassName(i)); }}}Copy the code

The handleResultSet() user processes the result set by first creating a DefaultResultHandler with a List inside, then calling handleRowValues() to process the returned row records and add the processing results to the List.

/** * process the result set, convert the result set to a Java object and store multipleResults *@paramRSW result set wrapper object *@paramResultMap indicates the resultMap. The resultType attribute is converted to a resultMap *@paramMultipleResults the final Java object result set@paramParentMapping <resultMap> Specifies the mapping between columns and attributes *@throws SQLException
 */
private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
  try {
    if(parentMapping ! =null) {
      handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
    } else { // We are not matched, so we go here
      /** If there is no ResultHandler, return the result. If there is a ResultHandler, it will give the results to it and not return the List result set. * /
      if (resultHandler == null) {
        /** ObjectFactory:MyBatis relies on it to create objects and assign values to them. DefaultResultHandler: the DefaultResultHandler. * /
        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
        // Process the returned data rows, convert them into Java object collections, and store them in DefaultResultHandler
        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
        // Store the multipleResults for the converted Java result set and return the result
        multipleResults.add(defaultResultHandler.getResultList());
      } else {
        handleRowValues(rsw, resultMap, resultHandler, rowBounds, null); }}}finally {
    // issue #228 (close resultsets)closeResultSet(rsw.getResultSet()); }}Copy the code

If ResultMap does not contain nested mapping results, will be called handleRowValuesForSimpleResultMap () method, it can handle paging, skip the part of the data, and then parse Discriminator Discriminator, It then calls getRowValue() to get the row results and convert them to Java objects, which are finally stored in the List.

/** * Processing result result mapping, not including nesting *@param rsw
 * @param resultMap
 * @param resultHandler
 * @param rowBounds
 * @param parentMapping
 * @throws SQLException
 */
private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler
        resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
  throws SQLException {
  DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
  ResultSet resultSet = rsw.getResultSet();
  // Skip some data according to paging rules
  skipRows(resultSet, rowBounds);
  // If there is more data, process it
  while(shouldProcessMoreRows(resultContext, rowBounds) && ! resultSet.isClosed() && resultSet.next()) {// Determine the Discriminator
    ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
    // Get the Java object from the result set
    Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
    // Store the single result object in the liststoreObject(resultHandler, resultContext, rowValue, parentMapping, resultSet); }}Copy the code

GetRowValue () first calls createResultObject() to create the result object using reflection. If the result object is complex and is empty, it must be returned with an applyAutomaticMappings() attribute assignment.

/** * Get the result from the row **@paramRSW result set wrapper object *@paramResultMap Indicates the result set mapping *@paramColumnPrefix Column name prefix *@return
 * @throws SQLException
 */
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
  final ResultLoaderMap lazyLoader = new ResultLoaderMap();
  /** create the result object. 1. If the object is simple, such as Long, then the result is already obtained. 2. If it is a complex object, such as a custom User, the object is empty, and the following attributes need to be assigned. * /
  Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
  if(rowValue ! =null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
    // Get the object metadata
    final MetaObject metaObject = configuration.newMetaObject(rowValue);
    // Use the constructor mapping
    boolean foundValues = this.useConstructorMappings;
    // Reflection assigns a value to the properties of an empty object
    if (shouldApplyAutomaticMappings(resultMap, false)) {
      foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
    }
    foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
    foundValues = lazyLoader.size() > 0 || foundValues;
    rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
  }
  // Return the assigned result object
  return rowValue;
}
Copy the code

CreateResultObject () reflects the creation of the result object according to the constructor:

private Object createResultObject(ResultSetWrapper rsw, ResultMap resultMap, ResultLoaderMap lazyLoader, String columnPrefix) throws SQLException {
  this.useConstructorMappings = false; // reset previous mapping result
  // The constructor parameter type list
  finalList<Class<? >> constructorArgTypes =new ArrayList<>();
  // The constructor argument list
  final List<Object> constructorArgs = new ArrayList<>();
  /** Create the result object. 1. If there is a TypeHandler for the return result class, it will be handled directly. 2. If the result is a complex class, such as a custom User, the constructor is called to create an empty object. * /
  Object resultObject = createResultObject(rsw, resultMap, constructorArgTypes, constructorArgs, columnPrefix);

  // The result exists and is a complex object
  if(resultObject ! =null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
    final List<ResultMapping> propertyMappings = resultMap.getPropertyResultMappings();
    for (ResultMapping propertyMapping : propertyMappings) {
      // issue gcode #109 && issue #149
      if(propertyMapping.getNestedQueryId() ! =null && propertyMapping.isLazy()) {
        resultObject = configuration.getProxyFactory().createProxy(resultObject, lazyLoader, configuration, objectFactory, constructorArgTypes, constructorArgs);
        break; }}}this.useConstructorMappings = resultObject ! =null && !constructorArgTypes.isEmpty(); // set current mapping result
  return resultObject;
}
Copy the code

ApplyAutomaticMappings () is used to assign values to the resulting object attributes:

// Assign a value to the result object
private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
  List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
  boolean foundValues = false;
  if(! autoMapping.isEmpty()) {for (UnMappedColumnAutoMapping mapping : autoMapping) {
      final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
      if(value ! =null) {
        foundValues = true;
      }
      if(value ! =null|| (configuration.isCallSettersOnNulls() && ! mapping.primitive)) {// gcode issue #377, call setter on nulls (value is not 'found')metaObject.setValue(mapping.property, value); }}}return foundValues;
}
Copy the code

The end result will be a User object in the List, and then selectOne() will return the 0th element in the List, so the proxy object will return the final result.

4. To summarize

MyBatis will first parse the Configuration file to create a Configuration object, and then use the Configuration to build a SqlSessionFactory, with SqlSessionFactory can open a SqlSession. Based on SqlSession, we can get the MapperProxy object of Mapper interface. When we call the query method of Mapper, the proxy object will automatically call the SELECT method of SqlSession for us. SqlSession will delegate Executor to call the query method, then create the corresponding JDBC native Statement according to StatementType and complete the parameter setting, and finally execute SQL to get the ResultSet ResultSet. Finally, the result mapping is completed according to the ResultMap, the ResultSet is transformed into the target result Java Bean, and finally returned through the proxy object.

There are too many things, some may not be clear in three or two sentences, I will skip, in the future for the core class to write a separate article record, please look forward to ~