MyBatis query whole process source code analysis

As an excellent persistence layer framework, MyBatis has been used by more and more companies. In the face of this framework we use every day, how can we not read its source code well? I spent a few days reading and debugging the MyBatis source code. Now I share some of my understanding with you. If there are errors, please correct them.

MyBatis source code version: 3.5.8-SNAPSHOT, source code address: https://github.com/mybatis/mybatis-3.git

1. Preface

The overall framework of MyBatis is roughly shown in the figure below, in which each module can be carried out to write an article. Because of length, this article will only introduce the whole process of [query], and will not introduce too many details. For example, xml parsing, parameter mapping and caching will be covered in one stroke, and the details will be introduced separately in the following articles.

This article only discusses MyBatis and only analyzes the source code of MyBatis, so it will not integrate with Spring.

2. Sample program

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

1. Write mybatis config XML 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>
  <!--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 to configure-->
  <mappers>
    <mapper resource="mappers/UserMapper.xml"/>
  </mappers>
</configuration>

2. 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 Getter,Setter method
}

3. Write the UserMapper interface.

public interface UserMapper {

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

4. Write usermapper XML 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="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>

5. Write test program.

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

At this point, all the sample programs are finished. Run the sample program, and the console will output the user information with ID=1. Based on this program, we step by step analyze how MyBatis can realize the mapping from database query to Java Bean with only one interface and xml file.

3. Source code analysis

3.1 construction of sqlsessionfactory

SqlSessionFactory is a factory for MyBatis to manage replies. Its responsibility is to open a SqlSession. With SqlSession, we can add, delete, modify and query the database.

SqlSessionFactory is an interface, and the default implementation class is DefaultSqlSessionFactory.

Some codes are posted here:

public interface SqlSessionFactory {

  // Open a new reply
  SqlSession openSession()

  /**
   * Open a new reply
   * @param autoCommit Auto submit
   * @return
   */
  SqlSession openSession(boolean autoCommit);

  /**
   * Opens a new reply at the specified transaction isolation level
   * @param level Transaction isolation level
   * @return
   */
  SqlSession openSession(TransactionIsolationLevel level);
    
  // Get global configuration
  Configuration getConfiguration();
}

SqlSessionFactory is built in the builder mode. The corresponding class is SqlSessionFactoryBuilder. The build() construction method has multiple overloads. You can build it through the character stream Reader, the byte stream InputStream, or the global Configuration object Configuration. In fact, no matter which method is used, it is finally built through the Configuration object. The first two methods of building MyBatis are just to add config. From the character stream / byte stream The XML Configuration file is parsed into a Configuration object.

public class SqlSessionFactoryBuilder {
  // Build from character stream
  public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
  }
  
  // Build from byte stream
  public SqlSessionFactory build(InputStream inputStream) {
    return build(inputStream, null, null);
  }
  
  // It is built by configuring objects, and finally it is built by this method
  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }    
}

3.2 construction of configuration

Configuration is the global configuration class of MyBatis, which can be regarded as MyBatis config The description object of XML configuration file in Java. This class is very important and huge. I won't elaborate here. I will write article records later.

Configuration is required to build SqlSessionFactory. How does the configuration object come from? In fact, there are two ways: one is to manually create a configuration object, and the other is to write an xml file and let MyBatis parse it by itself.

Generally, configuration files are used, so we focus on xmlconfigbuilder parse().

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

// Parse from root node
private void parseConfiguration(XNode root) {
  try {
    /**
     * The following is to parse all kinds of tags in turn
     * 1.Parse the properties tag and read the properties
     * 2.Parse the settings tag and read the settings
     * 3.Resolve alias of class
     * 4.Resolve plug-in configuration
     * 5.Resolve object factory configuration
     * 6.Resolve object packaging factory configuration
     * 7.Analyze the running environment and configure multiple data sources
     * 8.Parsing databaseIdProvider, multi database support
     * 9.Resolve typeHandlers, type handler
     * 10.Resolve mappers and register 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);
  }
}

3.3 SqlSession

Parse the Configuration file to generate a Configuration object. With Configuration, you can build SqlSessionFactory, and with SqlSessionFactory, you can obtain SqlSession objects.

SqlSession is one of the most important interfaces of MyBatis. It is the only interface provided by MyBatis for developers to operate the database, which greatly simplifies the operation of the database.

Let's take a look at the interface definition first. We will know what capabilities it has. I only post part of the 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 data
    int update(String statement);
    
    // Delete data
    int delete(String statement);
    
    // Commit transaction
    void commit();
    
    // Rollback transaction
    void rollback();
    
    // Gets the proxy class of the Mapper interface
    <T> T getMapper(Class<T> type);
    
    // Gets the database connection associated with SqlSession
    Connection getConnection();
}

It can be seen through the interface that as long as there is a SqlSession object, we can add, delete, modify and query the database and operate transactions, and it will automatically help us map the result set and Java objects, which is very convenient.

3.4 generating Mapper proxy objects

Generally, we rarely operate the database directly through SqlSession, but create an interface to operate through the proxy object generated by MyBatis. Therefore, we focus on the getMapper() method.

The default implementation class of SqlSession is DefaultSqlSession. Just look at it directly.

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

When we get Mapper's proxy object through SqlSession, it will be handed over to the Configuration object for completion. Because when MyBatis parses the Configuration file, Mapper.com will be parsed together XML file and register the parsing results in MapperRegistry. MapperRegistry is the Registrar of Mapper interface. It puts Mapper interface Class and corresponding MapperProxyFactory into a Map container. You can register Mapper interface and obtain the proxy object of Mapper interface.

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

  // Gets 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 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);
        }
      }
    }
  }
}

When registering the Mapper interface with MapperRegistry, it encapsulates the Mapper interface as MapperProxyFactory because it needs to be relied on to create proxy objects for the Mapper interface. The logic of creating a proxy object is also very simple, because Mapper is an interface, so you can directly use JDK dynamic proxy to generate the MapperProxy of the 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 calling a method through a proxy object, the method will first be parsed into MapperMethodInvoker
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

  @SuppressWarnings("unchecked")
  // Create a new instance and create a proxy object through JDK dynamic proxy
  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 some code
}

3.5 MapperProxy

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

UserMapper is an interface that cannot be instantiated. Where does the mapper object come from?
When we call the first line of code, MyBatis actually generates a proxy object MapperProxy for us through JDK dynamic proxy. When we call the second line of code, we actually execute MapperProxy Invoke() method.

The invoke() of the proxy Object is as follows. If the method of the Object class is called, such as hashCode and equals, it can be called directly through the proxy Object itself without operating the database. Otherwise, a MapperMethodInvoker Object is generated for it and its invoke method is called.

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    if (Object.class.equals(method.getDeclaringClass())) {
      // If it is a method inherited from the Object class, it can be called directly through the proxy Object itself
      return method.invoke(this, args);
    } else {
      /**
      Only custom methods operate the database
      1.First get the MapperMethodInvoker corresponding to the method from the cache
      2.implement
      For non default violations, see: org apache. ibatis. binding. MapperProxy. PlainMethodInvoker. invoke()
      @see MapperMethod#execute(org.apache.ibatis.session.SqlSession, java.lang.Object[])
       */
      return cachedInvoker(method).invoke(proxy, method, args, sqlSession);
    }
  } catch (Throwable t) {
    throw ExceptionUtil.unwrapThrowable(t);
  }
}

MapperMethodInvoker is the wrapper class of MyBatis for the Method in Mapper interface. It has two implementation classes: DefaultMethodInvoker and PlainMethodInvoker.

DefaultMethodInvoker is a wrapper for the default method in Mapper interface and does not need to operate the database. You can directly obtain the handle object of the method and call MethodHandle.

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

// Get the MapperMethodInvoker object corresponding to the Method from the cache
private MapperMethodInvoker cachedInvoker(Method method) throws Throwable {
  try {
    return MapUtil.computeIfAbsent(methodCache, method, m -> {
      if (m.isDefault()) {
        // If the default method in the Interface is called, find the method handle MethodHandle and call it through reflection.
        // Not the point, just understand.
        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 {
        // For non default methods that need to operate the database, generate PlainMethodInvoker objects.
        // It will operate the database through sqlSession.
        return new PlainMethodInvoker(new MapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
      }
    });
  } catch (RuntimeException re) {
    Throwable cause = re.getCause();
    throw cause == null ? re : cause;
  }
}

PlainMethodInvoker needs a MapperMethod object. For methods that need to operate the database, it will be transferred to MapperMethod for completion, and it will call MapperMethod Execute() method.

3.6 MapperMethod

MapperMethod is MyBatis's wrapper class for the methods that need to operate the database in the Mapper interface. First look at its properties:

/**
 * Encapsulated class of SQL command executed
 * name:Interface fully qualified name + method name, locate the unique sql to be executed
 * type:SQL Command type (addition, deletion, modification and query)
 */
private final SqlCommand command;
/**
 * Method signature description class
 * What is the return value of the method?
 * Return a single object or a collection object?
 * Return to Map?
 * wait...
 */
private final MethodSignature method;

SqlCommand is a description of the SQL command to be executed by the method. It records the unique identification Statement of the SQL to be executed and the type of SQL command.

public static class SqlCommand {
    // Interface fully qualified name + method name, locate the unique sql to be executed
    private final String name;
    // SQL command type: add, delete, modify, query, etc
    private final SqlCommandType type;
}

MethodSignature is a description of the method signature. It records the parameters, return values, paging, ResultHandler and other information of the method.

public static class MethodSignature {

    // Return multiple objects? Such as List
    private final boolean returnsMany;
    // Return to Map?
    private final boolean returnsMap;
    // Return Void?
    private final boolean returnsVoid;
    // Return Cursor object?
    private final boolean returnsCursor;
    // Return Optional object?
    private final boolean returnsOptional;
    // Return type Class
    private final Class<?> returnType;
    // Parse the value of the @ MapKey annotation on the method
    private final String mapKey;
    // Subscript of the parameter list where ResultHandler is located (processing query results)
    private final Integer resultHandlerIndex;
    // Subscript of the parameter list where RowBounds is located (limit the number of returned results)
    private final Integer rowBoundsIndex;
    // Parameter name parser. Parameters annotated with @ Param can be directly used in xml
    private final ParamNameResolver paramNameResolver;
    // Omit part of the code
}

When we execute the Mapper interface to operate the database, the MapperProxy proxy object will execute mappermethod Execute() method, where the [add, delete, modify query] operation of the database will be completed, which needs special attention.

Space reason, here only look at the query. When calling usermapper When getbyid (1L), it will first judge whether the method returns void and the parameter contains ResultHandler. If so, no result will be returned, and the query result will be handed over to ResultHandler for processing.
Then it will judge whether to return multiple results? Return to Map? Return cursor? Obviously, there is no satisfaction here, so go straight to the last else.

When executing a normal query, it first needs to parse the method parameters into ParamMap, which is essentially a HashMap. Key is the parameter Name and Value is the parameter Value. The purpose of parameter parsing is that you can use #{id}/${id} in xml to use parameters, so as to realize dynamic SQL.

When the parameter resolution is complete, it calls SqlSession Selectone() queries a single result. See, we still operate the database through SqlSession, but we rarely use it directly. We operate through proxy objects.

After the query is completed, judge whether the return type is Optional. If so, it will wrap the result with Optional and finally return 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 operation
      Object param = method.convertArgsToSqlCommandParam(args);
      result = rowCountResult(sqlSession.delete(command.getName(), param));
      break;
    }
    case SELECT:// Query operation
      if (method.returnsVoid() && method.hasResultHandler()) {
        /**
        The return value of the method is Void, and there is a ResultHandler in the parameter. The query result is handed over to the ResultHandler for processing, and null is returned directly
         @see ResultHandler
         */
        executeWithResultHandler(sqlSession, args);
        result = null;
      } else if (method.returnsMany()) {
        // Return multiple results
        result = executeForMany(sqlSession, args);
      } else if (method.returnsMap()) {
        // Return to Map
        result = executeForMap(sqlSession, args);
      } else if (method.returnsCursor()) {
        // Returns the Cursor object Cursor
        result = executeForCursor(sqlSession, args);
      } else {
        /*
        The parameter mapping required to convert method arguments into SQL statements, so that you can use parameters in xml through #{param}.
        Generally, a Map structure is returned: for example
          "id":1,
          "param1":1
         */
        Object param = method.convertArgsToSqlCommandParam(args);
        /*
        Execute SQL query
        1.Navigate to MappedStatement according to the statement
        2.SQL Statement processing, parameter binding
        3.StatementHandler Create corresponding Statement
        4.Execute SQL
        5.The result set is processed and the ResultSet is converted into a Java Bean
         */
        result = sqlSession.selectOne(command.getName(), param);
        // If the returned result is Optional, the result will be wrapped automatically.
        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;
}

3.7 ParamNameResolver

ParamNameResolver is the [parameter name resolver] provided by MyBatis. It will process the formal parameters of the method and the parameters annotated with @ Param, and convert them into ParamMap.

First look at the properties:

public class ParamNameResolver {

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

  // Whether to use the actual parameter name, you may get meaningless names such as arg0 and arg1
  private final boolean useActualParamName;
    
  // Parameter subscript - corresponding parameter name
  private final SortedMap<Integer, String> names;

  // Is there a @ Param annotation
  private boolean hasParamAnnotation;
}

When the MethodSignature object is created, the corresponding ParamNameResolver will be created.

ParamNameResolver will resolve the parameter Name and its corresponding subscript in the constructor and store it in names. The logic of parsing is: take the Name specified by @ Param annotation first. If there is no annotation, judge whether to take the actual parameter Name. Otherwise, take the parameter subscript as the Name.

Here's a supplement. Java reflection can obtain parameter names on the premise that the JDK8 version is required and the - parameters parameter is added during compilation. Otherwise, meaningless parameter names such as arg0 and arg1 are obtained.

/**
 * 1.Get the value of @ Param annotation first
 * 2.Get parameter name through reflection
 * 3.Use parameter subscript
 */
public ParamNameResolver(Configuration config, Method method) {
  this.useActualParamName = config.isUseActualParamName();
  final Class<?>[] 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 RowBounds and ResultHandler parameters
      continue;
    }
    String name = null;
    for (Annotation annotation : paramAnnotations[paramIndex]) {
      if (annotation instanceof Param) {
        // If @ Param annotation is added, the annotation value will be taken
        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 through reflection.
        // The JDK8 compilation class can retain the parameter name by adding the - parameters parameter. Otherwise, it will get meaningless parameter names such as arg0 and arg1
        name = getActualParamName(method, paramIndex);
      }
      if (name == null) {
        // If the name is still empty, you can use subscripts to get parameters: #{param1},#{param2}
        name = String.valueOf(map.size());
      }
    }
    map.put(paramIndex, name);
  }
  // Make it immutable
  names = Collections.unmodifiableSortedMap(map);
}

The constructor is only responsible for resolving the parameter name corresponding to the parameter subscript. To obtain the parameter value corresponding to the parameter name, you also need to call the getNamedParams() method.
Because developers may not annotate, reflection may not get the parameter name. Due to various uncertainties, MyBatis provides an additional deterministic scheme. When parsing parameters, it will not only use known parameter names, but also automatically generate records according to #{param subscript} according to the parameter subscript.

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

"id" 	 > 1
"param1" > 1

The code is as follows:

/**
 * Get the parameter value corresponding to the parameter name
 * Generally, it is a Map structure. When there is only one parameter, it is returned directly. Any value written in xml can be matched
 * @param args
 * @return
 */
public Object getNamedParams(Object[] args) {
  final int paramCount = names.size();
  if (args == null || paramCount == 0) {
    // No parameter condition
    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);
      // Additional param1,param2 You can also use #{param1} in xml
      if (!names.containsValue(genericParamName)) {
        param.put(genericParamName, args[entry.getKey()]);
      }
      i++;
    }
    return param;
  }
}

3.8 selectOne

After the parameter parsing is completed, it returns to the query operation of SqlSession. Let's talk about why SqlSession can operate the database. To operate the database, you must first get the database Connection.

DefaultSqlSession is the default implementation class of SqlSession. Let's take a look at its creation process.

When we open a new reply from SqlSessionFactory, we call the openSessionFromDataSource() method. It first obtains the running environment from the configuration object, and then obtains the transaction factory TransactionFactory from the environment.
TransactionFactory corresponds to the following configuration items in the configuration file, which have been created when parsing the configuration file, generally JdbcTransactionFactory.

<transactionManager type="JDBC"/>

The JdbcTransactionFactory will open a new transaction JdbcTransaction, which contains the data source and database Connection.

public class JdbcTransaction implements Transaction {
  // Database connection
  protected Connection connection;
  // data source
  protected DataSource dataSource;
  // Transaction isolation level
  protected TransactionIsolationLevel level;
  // Auto submit
  protected boolean autoCommit;
}

With JdbcTransaction, create the corresponding executor according to the ExecutorType. Executor is the executor interface of MyBatis operation database.
With the Executor, you can create DefaultSqlSession. The source code is as follows:

/**
 * Open a Session from DataSource
 * @param execType Actuator type: simple, reusable, batch
 * @param level Transaction isolation level
 * @param autoCommit Auto submit
 * @return
 */
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
  Transaction tx = null;
  try {
    // Get runtime environment: created when parsing xml
    final Environment environment = configuration.getEnvironment();
    // Gets the transaction factory associated with the Environment
    final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
    // Use the factory class to create a transaction manager, usually JdbcTransaction
    tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
    // Create executors based on transaction manager and executor type, generally simpleexecution
    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();
  }
}

With SqlSession, you can operate the database. Back to the selectOne() method, it will still execute selectList(), but it will automatically take the 0 result. Therefore, we focus on selectList().

Statement is the unique identifier of the SQL to be executed, which is composed of [interface fully qualified name + method name]. Therefore, MyBatis does not support overloading of interface methods. According to the statement, you can locate the only SQL node in xml, which is represented by MappedStatement class in Java.

MappedStatement is also very important. It will be described in detail in the next section. Now you only need to know that it represents the SQL tag node. With it, you can know what the SQL statement to be executed and what the return result is.

After you locate the MappedStatement, you call the executor Query() has performed the query operation.

/**
 * Query multiple results
 * @param statement Unique identification of SQL to be executed: fully qualified interface name + method name
 * @param parameter parameter list
 * @param rowBounds Paging condition
 * @param handler The result processor is null by default if there is no ResultHandler in the parameter
 * @param <E>
 * @return
 */
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
  try {
    // Get mappedstatement from the Map cache (created when parsing SQL tags in xml).
    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();
  }
}

3.9 MappedStatement

MappedStatement can be understood as MyBatis's description of the [add, delete, modify query] SQL node in the xml file. MyBatis will automatically generate the object when parsing the xml file. The detailed code is in xmlstatementbuilder Parsestatementnode(), which will not be described in detail here.

The MappedStatement object records the xml file from which the SQL node comes, what the SQL statement is, what the return result type is, and so on. With it, you will know what SQL statements to execute and how to map the data of the result set. Therefore, this class is also very important.

The class is relatively large. For reasons of length, only the description of attributes is given:

public final class MappedStatement {

  // From which xml file
  private String resource;
  // Global configuration
  private Configuration configuration;
  // Unique identification: fully qualified interface name + method name
  private String id;
  // Limit the maximum number of rows returned by SQL execution to avoid OOM caused by queries with large amount of data
  private Integer fetchSize;
  // Timeout for SQL execution
  private Integer timeout;
  // Statement type used to execute SQL
  private StatementType statementType;
  // Returned result set type
  private ResultSetType resultSetType;
  // The executed SQL source through which to obtain the executed SQL statement
  private SqlSource sqlSource;
  // L2 cache
  private Cache cache;
  // Encapsulation of parameterMap attribute in tag
  private ParameterMap parameterMap;
  // The < resultmap > tag is encapsulated to configure the mapping relationship between database fields and Java class attributes
  private List<ResultMap> resultMaps;
  // Do you need to refresh the cache
  private boolean flushCacheRequired;
  // Use cache
  private boolean useCache;
  private boolean resultOrdered;
  // Type of SQL command (add, delete, modify, query)
  private SqlCommandType sqlCommandType;
  // The primary key generator returns the primary key when insert ing data
  private KeyGenerator keyGenerator;
  // Attributes applied by the primary key
  private String[] keyProperties;
  // Columns to which the primary key applies
  private String[] keyColumns;
  // Are there nested results
  private boolean hasNestedResultMaps;
  // Data source ID, distinguishing multiple data sources
  private String databaseId;
  // journal
  private Log statementLog;
  // Different language driven
  private LanguageDriver lang;
  // Used when returning multiple result sets
  private String[] resultSets;
}

3.10 Executor

The Executor interface is the Executor interface of the operation database provided by MyBatis. The SqlSession operation database is entrusted to the Executor for execution.

Post some definitions of Executor to see its capabilities, mainly database operation, transaction management, cache management, etc.

public interface Executor {

  /**
   * Execute SQL query
   * @param ms The executed Statement is understood as: which sql tag node of which xml is executed?
   * @param parameter parameter
   * @param rowBounds Paging data
   * @param resultHandler Result processor
   * @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() throws SQLException;

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

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

  // Create cache key for L1 cache
  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

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

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

Executor has an abstract subclass BaseExecutor, which is the parent class of other implementation classes. It adopts the template method mode to realize the basic functions.

Subclasses are as follows:

Implementation classexplain
SimpleExecutorA simple actuator that creates a new Statement each time it executes
ReuseExecutorReuse the executor and cache the same SQL Statement to avoid frequent Statement creation and optimize performance
BatchExecutorBatch operation actuator
CachingExecutorActuator supporting L2 cache, decorator mode

Caching is enabled by default, so let's look directly at caching executor query(). First, it will call ms.getBoundSql() to complete SQL parsing. This step will complete the dynamic splicing of SQL and complete the replacement of ${} / #{} parameters.
Then create the cache key CacheKey, which is the first level cache based on SqlSession provided by MyBatis. It is created according to the rules of StatementID+SQL + parameters + paging. Only these data are exactly the same can they hit the cache.
Then you call query() to query.

@Override
public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  // Complete the SQL parsing according to the arguments, replace #{} and ${}, and get the SQL that can be executed directly
  BoundSql boundSql = ms.getBoundSql(parameterObject);
  //System.err.println("###SQL:" + boundSql.getSql());
  // Create cache keys according to the executed [StatementID+SQL + parameter + paging]. Only these data are all the same can they hit the cache.
  CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
  return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

Cacheingexecution supports L2 cache. It uses decorator mode and internally relies on an Executor. Cacheingexecution itself does not process database operations. Its role is to judge whether the query hits L2 cache. If it does not hit L2 cache, delegate to query, and then cache the results.
The following is the query() method overridden by cachengexecution.

@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 enables L2 cache
    // Determine whether the cache needs to be emptied
    flushCacheIfRequired(ms);
    // Whether the current Statement uses caching
    if (ms.isUseCache() && resultHandler == null) {
      // If a stored procedure is called, the L2 cache does not support saving parameters of output type, and an exception will be thrown
      ensureNoOutParams(ms, boundSql);
      @SuppressWarnings("unchecked")
      // Get data from cache
      List<E> list = (List<E>) tcm.getObject(cache, key);
      if (list == null) {
        // If there is no in the cache, query the database and cache the results
        list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
        tcm.putObject(cache, key, list); // issue #578 and #116
      }
      return list;
    }
  }
  // delegate to query
  return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}

If the L2 cache is not hit, BaseExecutor. Is called Query(). The BaseExecutor will first determine whether the L1 cache is hit. If not, it will really query the database.

@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 making 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 refresh the cache, empty the local cache
    clearLocalCache();
  }
  List<E> list;
  try {
    queryStack++;
    // An attempt was made to get data from the L1 cache
    list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
    if (list != null) {
      handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
    } else {
      // query data base
      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;
}

When no cache is hit, doquery () is called to query the real database. Let's look directly at simpleexecution doQuery().
It first creates StatementHandler, then prepares JDBC native Statement, and finally calls JDBC native Statement.. Execute () to execute SQL, and then map the result set to get the final result.

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

3.11 StatementHandler

StatementHandler is the processor description of Statement in MyBatis. When the Executor wants to execute SQL, it will pass configuration Newstatementhandler() creates a StatementHandler first. The RoutingStatementHandler is created by default.

The [delegation mode] used by RoutingStatementHandler does not work itself, but creates a corresponding StatementHandler according to the StatementType and delegates it to 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:// ordinary
      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:// Precompiled, support setting parameters
      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:// Support calling stored procedures
      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
      throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  }

}

The StatementType enumeration has three instances, corresponding to three JDBC statements, as follows:

public enum StatementType {
  STATEMENT, // Statement is normal SQL, unable to set parameters
  PREPARED,  // PreparedStatement is precompiled to prevent SQL injection
  CALLABLE   // CallableStatement supports calling stored procedures
}

Correspondence between StatementHandler and JDBC statement:

StatementHandlerJDBC Statement
SimpleStatementHandlerStatement
PreparedStatementHandlerPreparedStatement
CallableStatementHandlerCallableStatement

Let's take a look at the process of preparing the Statement: first call the instantiateStatement() method of the subclass to create the corresponding Statement, and then set the 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 {
    /*
    Create JDBC native Statement based on StatementType
    SimpleStatementHandler    > connection.createStatement()
    PreparedStatementHandler  > connection.prepareStatement()
    CallableStatementHandler  > connection.prepareCall()
     */
    statement = instantiateStatement(connection);
    // Set timeout
    setStatementTimeout(statement, transactionTimeout);
    // Set FetchSize. If xml is not configured, the
    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);
  }
}

The query here is based on the user id, and #{id} is used in the xml, so the PreparedStatementHandler is used. The Statement created by the PreparedStatementHandler is, of course, the 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 {
            return connection.prepareStatement(sql, keyColumnNames);
        }
    } else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
        return connection.prepareStatement(sql);
    } else {
        return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    }
}

After the PreparedStatement is created, the next step is to set the parameters. The code is in defaultparameterhandler setParameters().
It will find the corresponding TypeHandler according to the parameter type. TypeHandler is the type processor interface provided by MyBatis. It has two functions: one is to set parameters for Statement, and the other is to obtain results from 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 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 parameter is Long, so it is LongTypeHandler
        TypeHandler typeHandler = parameterMapping.getTypeHandler();
        JdbcType jdbcType = parameterMapping.getJdbcType();
        if (value == null && jdbcType == null) {
          /*
          When the parameter is not empty, MyBatis can infer the JdbcType from the Java type.
          When the parameter is empty, it cannot be inferred, and the default type is used: jdbctype OTHER
          Oracle In the database, JdbcType If other passes in NULL, an exception will be reported: invalid column type. At this time, you must specify 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);
        }
      }
    }
  }
}

Our parameter is Long id, so the corresponding parameter is LongTypeHandler. Look at its parameter setting process, which is actually very simple.

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

Once the PreparedStatement is created and the parameters are set, it can be executed directly. After execution, get the ResultSet of the result set, leaving the result set mapping, and convert the ResultSet into a Java Bean.

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

3.12 ResultSetHandler

ResultSetHandler is the result set processor provided by MyBatis. It is responsible for converting the result set returned by the database into the Java Bean we want.

The default implementation class is DefaultResultSetHandler, which first wrapps the JDBC native ResultSet into MyBatis's ResultSetWrapper, then calls handleResultSet() to process the result set, converts the single ResultSet into JavaBean and stores it in list, then processes it in cycles, and finally returns 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, it is wrapped as the ResultSetWrapper object of MyBatis
  ResultSetWrapper rsw = getFirstResultSet(stmt);

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

  // Handle resultSets property, multiple result sets
  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++;
    }
  }

  // If there is only a single result set, the 0th element of multipleResults is returned; otherwise, multipleResults is returned directly
  return collapseSingleResultList(multipleResults);
}

Why wrap ResultSet as ResultSetWrapper? To facilitate result mapping and type conversion.
The ResultSetWrapper records all the columns returned by the result set, as well as the Java type and JDBC type corresponding to the column, as well as the TypeHandler corresponding to the column. It is required for type conversion of the result set. For space reasons, some codes are posted here:

public class ResultSetWrapper {

  // JDBC native result set
  private final ResultSet resultSet;
  // TypeHandler registrar
  private final TypeHandlerRegistry typeHandlerRegistry;
  // Listing
  private final List<String> columnNames = new ArrayList<>();
  // Corresponding Class name
  private final List<String> classNames = new ArrayList<>();
  // Corresponding JdbcType
  private final List<JdbcType> jdbcTypes = new ArrayList<>();
  private final Map<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();
    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.resultSet = rs;
    // Get 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 their column names, jdbctypes, and corresponding Java classes
    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));
    }
  }
}

handleResultSet() the user handles the result set, which first creates a DefaultResultHandler, which has a List inside, then calls handleRowValues() to process the returned row record and adds the result to List.

/**
 * Process the result set, convert the result set into Java objects and store them in multipleResults
 * @param rsw             Result set wrapper object
 * @param resultMap       For result mapping, the resultType attribute will be converted to resultMap
 * @param multipleResults Final Java object result set
 * @param parentMapping   <resultMap>Mapping relationship between columns and attributes configured in
 * @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 don't deserve it, so we go here
      /**
       If there is no ResultHandler, the result is returned.
       If there is a ResultHandler, the result is handed over to it for processing, and the List result set is not returned.
       */
      if (resultHandler == null) {
        /**
         ObjectFactory:MyBatis Rely on it to create objects and assign values to objects.
         DefaultResultHandler:The default result processor.
         */
        DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
        // Process the returned data row records, convert them into a Java object collection, and store them in DefaultResultHandler
        handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
        // Store the converted Java result set into multipleResults and return it
        multipleResults.add(defaultResultHandler.getResultList());
      } else {
        handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
      }
    }
  } finally {
    // issue #228 (close resultsets)
    closeResultSet(rsw.getResultSet());
  }
}

If ResultMap does not contain nested result mappings, the handleRowValuesForSimpleResultMap() method will be called. It will process paging, skip part of the data, then parse the discriminator Discriminator, then call getRowValue() to get the row result and convert it to Java pair image, and finally deposit Java object into List.

/**
 * Process result mapping without 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()) {
    // Resolution Discriminator
    ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
    // Get Java object from result set
    Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
    // Store a single result object in the list
    storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
  }
}

getRowValue() will first call createResultObject() to create a result object using reflection. If the result object is a complex object and it is still an empty object, you need to call applyAutomaticMappings() to assign the attribute, and then it can be returned.

/**
 * Get results from data rows
 *
 * @param rsw          Result set wrapper object
 * @param resultMap    Result set mapping
 * @param columnPrefix Column name prefix
 * @return
 * @throws SQLException
 */
private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
  final ResultLoaderMap lazyLoader = new ResultLoaderMap();
  /**
   Create result object
   1.If it is a simple object, such as Long, the result has been obtained.
   2.If it is a complex object, such as a custom User, you get an empty object, and you need to assign the following attributes.
   */
  Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
  if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
    // Get object metadata
    final MetaObject metaObject = configuration.newMetaObject(rowValue);
    // Use constructor mapping
    boolean foundValues = this.useConstructorMappings;
    // Attribute assignment reflected to 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;
  }
  // Returns the result object after assignment
  return rowValue;
}

createResultObject() creates a 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
  // List of constructor parameter types
  final List<Class<?>> constructorArgTypes = new ArrayList<>();
  // Constructor argument list
  final List<Object> constructorArgs = new ArrayList<>();
  /**
   Create result object
   1.If the returned result class has a corresponding TypeHandler, it will be processed directly. For example, Long will be returned, and the type conversion will be completed directly here.
   2.If the returned 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);

  // Results exist and are complex objects
  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;
}

applyAutomaticMappings() is used to assign values to the properties of the result object:

// 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;
}

Finally, there will be a User object in the List, and then selectOne() will take the 0th element in the List and return, so that the proxy object can return the final result.

4. Summary

When the MyBatis program starts, it will first parse the Configuration file to create a Configuration object, and then use Configuration to build SqlSessionFactory. With SqlSessionFactory, you can open a SqlSession. Based on SqlSession, we can get MapperProxy, the proxy object of Mapper interface. When we call Mapper's query method, the proxy object will automatically call SqlSession's select method for us, and SqlSession will entrust the Executor to call query method, and then create the corresponding JDBC native Statement according to StatementType and complete parameter setting, Finally, execute SQL to get the ResultSet of the result set. Finally, complete the result mapping according to the ResultMap, convert the ResultSet into the target result Java Bean, and finally return it through the proxy object.

There are too many things. I may skip some if I can't explain them clearly. In the future, I will write a separate article record for the core class. Please look forward to it~

Keywords: Mybatis

Added by jviney on Fri, 14 Jan 2022 16:09:20 +0200