5

One, MyBatis complete example

Here, I will use an entry-level example to demonstrate how MyBatis works.

Note: The principles and source code in the later chapters of this article will also be explained based on this example. complete example source code address

1.1. Database preparation

In this example, CRUD operations need to be performed on a user table. The data model is as follows:

CREATE TABLE IF NOT EXISTS user (
    id      BIGINT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'Id',
    name    VARCHAR(10)         NOT NULL DEFAULT '' COMMENT '用户名',
    age     INT(3)              NOT NULL DEFAULT 0 COMMENT '年龄',
    address VARCHAR(32)         NOT NULL DEFAULT '' COMMENT '地址',
    email   VARCHAR(32)         NOT NULL DEFAULT '' COMMENT '邮件',
    PRIMARY KEY (id)
) COMMENT = '用户表';

INSERT INTO user (name, age, address, email)
VALUES ('张三', 18, '北京', 'xxx@163.com');
INSERT INTO user (name, age, address, email)
VALUES ('李四', 19, '上海', 'xxx@163.com');

1.2. Add MyBatis

If you use Maven to build the project, you need to put the following dependent code in the pom.xml file:

<dependency>
  <groupId>org.Mybatis</groupId>
  <artifactId>Mybatis</artifactId>
  <version>x.x.x</version>
</dependency>

1.3. MyBatis configuration

The XML configuration file contains the core settings for the MyBatis system, including the data source (DataSource) for obtaining the database connection instance and the transaction manager (TransactionManager) that determines the scope and control method of the transaction.

In this example, only the most simplified configuration is given. [Example] MyBatis-config.xml 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>
  <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/spring_tutorial?serverTimezone=UTC" />
        <property name="username" value="root" />
        <property name="password" value="root" />
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="Mybatis/mapper/UserMapper.xml" />
  </mappers>
</configuration>
Note: The above configuration file only specifies the data source connection mode and the mapping configuration file of the User table.

1.4 Mapper

1.4.1 Mapper.xml

Personally, the Mapper.xml file can be regarded as the JDBC SQL template of MyBatis. [Example] UserMapper.xml file.

The following is a complete Mapper file automatically generated by MyBatis Generator.

<?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="io.github.dunwu.spring.orm.mapper.UserMapper">
  <resultMap id="BaseResultMap" type="io.github.dunwu.spring.orm.entity.User">
    <id column="id" jdbcType="BIGINT" property="id" />
    <result column="name" jdbcType="VARCHAR" property="name" />
    <result column="age" jdbcType="INTEGER" property="age" />
    <result column="address" jdbcType="VARCHAR" property="address" />
    <result column="email" jdbcType="VARCHAR" property="email" />
  </resultMap>
  <delete id="deleteByPrimaryKey" parameterType="java.lang.Long">
    delete from user
    where id = #{id,jdbcType=BIGINT}
  </delete>
  <insert id="insert" parameterType="io.github.dunwu.spring.orm.entity.User">
    insert into user (id, name, age,
      address, email)
    values (#{id,jdbcType=BIGINT}, #{name,jdbcType=VARCHAR}, #{age,jdbcType=INTEGER},
      #{address,jdbcType=VARCHAR}, #{email,jdbcType=VARCHAR})
  </insert>
  <update id="updateByPrimaryKey" parameterType="io.github.dunwu.spring.orm.entity.User">
    update user
    set name = #{name,jdbcType=VARCHAR},
      age = #{age,jdbcType=INTEGER},
      address = #{address,jdbcType=VARCHAR},
      email = #{email,jdbcType=VARCHAR}
    where id = #{id,jdbcType=BIGINT}
  </update>
  <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select id, name, age, address, email
    from user
    where id = #{id,jdbcType=BIGINT}
  </select>
  <select id="selectAll" resultMap="BaseResultMap">
    select id, name, age, address, email
    from user
  </select>
</mapper>

1.4.2 Mapper.java

The Mapper.java file is the Java object corresponding to Mapper.xml. [Example] UserMapper.java file

public interface UserMapper {

    int deleteByPrimaryKey(Long id);

    int insert(User record);

    User selectByPrimaryKey(Long id);

    List<User> selectAll();

    int updateByPrimaryKey(User record);

}

Comparing UserMapper.java and UserMapper.xml files, it is not difficult to find that there is a one-to-one correspondence between the methods in UserMapper.java and the CRUD statement elements (<insert>, <delete>, <update>, <select>) in UserMapper.xml .

In MyBatis, it is the fully qualified name of the method that binds the two together.

1.4.3 Data entity.java

[Example] User.java file

public class User {
    private Long id;

    private String name;

    private Integer age;

    private String address;

    private String email;

}

The parameterType attribute of <insert>, <delete>, <update>, <select> and the type attribute of <resultMap> may be bound to data entities. In this way, the input and output of the JDBC operation can be combined with the JavaBean, which is more convenient and easy to understand.

1.5. Test Procedure

[Example] MyBatisDemo.java file

public class MyBatisDemo {

    public static void main(String[] args) throws Exception {
        // 1. 加载 MyBatis 配置文件,创建 SqlSessionFactory
        // 注:在实际的应用中,SqlSessionFactory 应该是单例
        InputStream inputStream = Resources.getResourceAsStream("MyBatis/MyBatis-config.xml");
        SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
        SqlSessionFactory factory = builder.build(inputStream);

        // 2. 创建一个 SqlSession 实例,进行数据库操作
        SqlSession sqlSession = factory.openSession();

        // 3. Mapper 映射并执行
        Long params = 1L;
        List<User> list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params);
        for (User user : list) {
            System.out.println("user name: " + user.getName());
        }
        // 输出:user name: 张三
    }

}

Description: The SqlSession interface is the core of the MyBatis API core, and it represents a complete session between MyBatis and the database.

  • MyBatis will parse the configuration and create a SqlSession based on the configuration.
  • Then, MyBatis maps Mapper to SqlSession, then passes parameters, executes SQL statements, and obtains results.

Two, MyBatis life cycle

2.1. SqlSessionFactoryBuilder

2.1.1 Responsibilities of SqlSessionFactoryBuilder

SqlSessionFactoryBuilder is responsible for creating SqlSessionFactory instances.

SqlSessionFactoryBuilder can construct an instance of SqlSessionFactory from an XML configuration file or an instance of a pre-customized Configuration.

The Configuration class contains all the content you may be concerned about with an instance of SqlSessionFactory.

SqlSessionFactoryBuilder applies the builder design pattern. It has five build methods that allow you to create SqlSessionFactory instances from different resources.

SqlSessionFactory build(InputStream inputStream)
SqlSessionFactory build(InputStream inputStream, String environment)
SqlSessionFactory build(InputStream inputStream, Properties properties)
SqlSessionFactory build(InputStream inputStream, String env, Properties props)
SqlSessionFactory build(Configuration config)

2.1.2 The life cycle of SqlSessionFactoryBuilder

SqlSessionFactoryBuilder can be instantiated, used and discarded. Once the SqlSessionFactory is created, it is no longer needed. Therefore, the best scope of an instance of SqlSessionFactoryBuilder is method scope (that is, local method variables).

You can reuse SqlSessionFactoryBuilder to create multiple SqlSessionFactory instances, but it is better not to keep it forever to ensure that all XML parsing resources can be released for more important things.

2.2. SqlSessionFactory

2.2.1 SqlSessionFactory Responsibilities

SqlSessionFactory is responsible for creating SqlSession instances.

SqlSessionFactory applies the factory design pattern, which provides a set of methods for creating SqlSession instances.

SqlSession openSession()
SqlSession openSession(boolean autoCommit)
SqlSession openSession(Connection connection)
SqlSession openSession(TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType, TransactionIsolationLevel level)
SqlSession openSession(ExecutorType execType)
SqlSession openSession(ExecutorType execType, boolean autoCommit)
SqlSession openSession(ExecutorType execType, Connection connection)
Configuration getConfiguration();

method description:

The default openSession() method has no parameters, it will create a SqlSession with the following characteristics:

1) The transaction scope will be turned on (that is, not automatically committed).

  • The Connection object will be obtained from the DataSource instance configured by the current environment.
  • The transaction isolation level will use the default setting of the driver or data source.
  • Prepared statements will not be reused, and updates will not be processed in batches.

2) TransactionIsolationLevel represents the transaction isolation level, which corresponds to the five transaction isolation levels of JDBC.

3) The ExecutorType enumeration type defines three values:

  • ExecutorType.SIMPLE: This type of executor has no special behavior. It creates a new prepared statement for the execution of each statement.
  • ExecutorType.REUSE: This type of executor will reuse prepared statements.
  • ExecutorType.BATCH: This type of executor will execute all update statements in batches. If SELECT is executed in the middle of multiple updates, multiple update statements will be separated if necessary to facilitate understanding.

2.2.2 SqlSessionFactory life cycle

SQLSessionFactory should always exist in the form of a singleton during the runtime of the application.

2.3. SqlSession

2.3.1 SqlSession Responsibilities

The main Java interface of MyBatis is SqlSession. It contains all methods for executing statements, obtaining mappers, and managing transactions. For details, please refer to: " MyBatis Official Document".

The methods of the SQLSession class can be roughly classified according to the following figure:

2.3.2 SqlSession life cycle

SqlSessions is created by an instance of SqlSessionFactory; and SqlSessionFactory is created by SqlSessionFactoryBuilder.

🔔 Note : When MyBatis is used with some dependency injection frameworks (such as Spring or Guice), SqlSessions will be created by the dependency injection framework, so you don't need to use SqlSessionFactoryBuilder or SqlSessionFactory.

Each thread should have its own SqlSession instance.

The instance of SqlSession is not thread-safe, so it cannot be shared, so its best scope is the request or method scope. You must not put the reference of the SqlSession instance in the static field of a class, even the instance variables of a class. Never place the reference of the SqlSession instance in any type of managed scope, such as HttpSession in the Servlet framework. The correct use of SqlSession in the Web is: every time you receive an HTTP request, you can open a SqlSession, return a response, and then close it.

Programming mode:

try (SqlSession session = sqlSessionFactory.openSession()) {  // 你的应用逻辑代码}

2.4. Mapper

2.4.1 Mapper Responsibilities

Mappers are interfaces created by users to bind SQL statements.

The insert, update, delete, and select methods in SqlSession are all powerful, but also a bit cumbersome. A more general way is to use the mapper class to execute the mapping statement. A mapper class is an interface class that only needs to declare methods that match the SqlSession methods.

MyBatis abstracts each <mapper> node in the configuration file as a Mapper interface, and the method declared in this interface corresponds to the <select|update|delete|insert> node in the <mapper> node, that is, <select| The id value of the update|delete|insert> node is the name of the method in the Mapper interface, the parameterType value represents the input parameter type of the Mapper corresponding method, and the resultMap value corresponds to the return value type represented by the Mapper interface or the element type of the return result set.

MyBatis will generate a Mapper instance through the dynamic proxy mechanism according to the method information declared by the corresponding interface; MyBatis will determine the Statement id according to the method name and parameter type of this method, and then map it with the SqlSession, and the bottom layer is to complete the interaction with the database through the SqlSession .

The following example shows some method signatures and how they are mapped to SqlSession.

Note:

2.4.2 Mapper life cycle

The instance of the mapper interface is obtained from SqlSession. So technically speaking, the maximum scope of any mapper instance is the same as the SqlSession that requested them. Nevertheless, the best scope of the mapper instance is the method scope. In other words, mapper instances should be requested in the method that calls them, and can be discarded after they are used.

Programming mode:

try (SqlSession session = sqlSessionFactory.openSession()) {
  BlogMapper mapper = session.getMapper(BlogMapper.class);
  // 你的应用逻辑代码
}

mapper annotation

MyBatis is an XML-driven framework. The configuration information is based on XML, and the mapping statement is also defined in XML. After MyBatis 3, annotation configuration is supported. The annotation configuration is based on the configuration API; and the configuration API is based on the XML configuration.

MyBatis supports annotations such as @Insert, @Update, @Delete, @Select, @Result, etc.

For details, please refer to: MyBatis official document sqlSessions , which enumerates the list of annotations supported by MyBatis and basic usage.

Third, the architecture of MyBatis

From the perspective of MyBatis code implementation, the main components of MyBatis are as follows:

  • SqlSession-As the main top-level API for MyBatis work, it represents the session that interacts with the database and completes the necessary database addition, deletion, modification, and query functions.
  • Executor-MyBatis executor is the core of MyBatis scheduling, responsible for SQL statement generation and query cache maintenance.
  • StatementHandler-encapsulates the JDBC Statement operation and is responsible for the operation of the JDBC statement, such as setting parameters and converting the Statement result set into a List set.
  • ParameterHandler-responsible for converting the parameters passed by the user into the parameters required by the JDBC Statement.
  • ResultSetHandler-responsible for converting the ResultSet object returned by JDBC into a collection of type List.
  • TypeHandler-responsible for the mapping and conversion between java data types and jdbc data types.
  • MappedStatement-MappedStatement maintains an encapsulation of the <select|update|delete|insert> node.
  • SqlSource-Responsible for dynamically generating SQL statements based on the parameterObject passed by the user, encapsulating the information in a BoundSql object, and returning it.
  • BoundSql-Represents dynamically generated SQL statements and corresponding parameter information.
  • Configuration-All configuration information of MyBatis is maintained in the Configuration object.

The architectural levels of these components are as follows:

3.1. Configuration layer

The configuration layer determines how MyBatis works.

MyBatis provides two configuration methods:

  • Based on XML configuration file
  • Java API-based approach

SqlSessionFactoryBuilder will create SqlSessionFactory according to the configuration;

SqlSessionFactory is responsible for creating SqlSessions.

3.2. Interface layer

The interface layer is responsible for the way of interacting with the database. There are two ways to interact between MyBatis and the database:

1) uses SqlSession : SqlSession encapsulates all execution statements, obtains the mapper and manages the transaction method.

  • Users only need to pass in Statement Id and query parameters to the SqlSession object, and they can interact with the database easily.
  • The disadvantage of this approach is that it does not conform to the paradigm of object-oriented programming.

2) uses the Mapper interface : MyBatis will generate a Mapper instance through the dynamic proxy mechanism according to the method information declared by the corresponding interface; MyBatis will determine the Statement Id according to the method name and parameter type of this method, and then map it with SqlSession, the bottom layer The interaction with the database is completed through SqlSession.

3.3. Data processing layer

The data processing layer can be said to be the core of MyBatis. In a big way, it has to complete two functions:

1) according to the passed parameter Statement and parameters

  • Dynamic statement generation can be said to be a very elegant design of the MyBatis framework. MyBatis uses Ognl to dynamically construct SQL statements through the parameter values passed in, making MyBatis highly flexible and extensible.
  • Parameter mapping refers to the conversion between java data type and jdbc data type: there are two processes here: in the query phase, we need to convert java type data into jdbc type data through preparedStatement.setXXX() Set the value; the other is to convert the jdbcType data of the resultset query result set into a java data type.

2) executes SQL statements and processes the response result set ResultSet

  • After the dynamic SQL statement is generated, MyBatis will execute the SQL statement and convert the result set that may be returned into a List<E> list.
  • In the processing of the result set, MyBatis supports one-to-many and many-to-one conversion of the result set relationship, and there are two support methods, one is the query of the nested query statement, and the other is the query of the nested result set .

3.4. Frame support layer

1) Transaction management mechanism-MyBatis abstracts the transaction into a Transaction interface. The transaction management of MyBatis is divided into two forms:

  • Use the JDBC transaction management mechanism: that is, use the java.sql.Connection object to complete the commit, rollback, close, etc. of the transaction.
  • Use MANAGED's transaction management mechanism: MyBatis does not implement transaction management by itself, but allows program containers such as (JBOSS, Weblogic) to achieve transaction management.

2) Connection pool management

3) SQL statement configuration-supports two ways:

  • xml configuration
  • Annotation configuration

4) Cache mechanism-MyBatis adopts a two-level cache structure;

  • Level 1 Cache is Session Level Cache-Level 1 Cache is also called local cache. Generally speaking, a SqlSession object will use an Executor object to complete the session operation, and the Executor object will maintain a Cache to improve query performance.
  1. The life cycle of the first level cache is at the Session level.
  • L2 cache is the application level cache-the user configures "cacheEnabled=true" to enable the L2 cache.
  1. If the second level cache is enabled, SqlSession will first use the CachingExecutor object to process query requests. CachingExecutor will check whether there is matching data in the second-level cache. If there is a match, it will directly return the cached result; if it is not in the cache, it will be handed over to the real Executor object to complete the query, and then CachingExecutor will place the query result returned by the real Executor To the cache, and then return to the user.
  2. The life cycle of the secondary cache is application level.

Fourth, the internal working mechanism of SqlSession

From the previous article, we have understood that MyBatis encapsulates the access to the database, and puts the session and transaction control of the database in the SqlSession object. So how does it work? Next, we analyze the source code interpretation.

SqlSession's internal processing mechanisms for insert, update, delete, and select are basically the same. So, next, I will take a complete select query process as an example to explain the internal working mechanism of SqlSession. I believe that if readers understand the process of select, they can also achieve Belden for other CRUD operations.

4.1 SqlSession subcomponent

The previous content has already introduced: SqlSession is the top-level interface of MyBatis, which provides methods for executing statements, obtaining mappers, and managing transactions.

In fact, SqlSession realizes the issuance of tasks by aggregating multiple sub-components and letting each sub-component be responsible for its own function.

Before understanding the working mechanism of each sub-component, let us briefly understand the core sub-components of SqlSession.

4.1.1 Executor

Executor is the executor, which is responsible for generating dynamic SQL and managing the cache.

  • Executor is the executor interface.
  • BaseExecutor

    It is an abstract class of Executor. It adopts the template method design pattern and has some common methods built-in, while the customized methods are left to subclasses to implement.

  • SimpleExecutor

    It is the simplest actuator. It will only execute SQL directly and will not do additional things.

  • BatchExecutor

    It is a batch executor. Its role is to optimize performance through batch processing. It is worth noting that for batch update operations, due to the internal caching mechanism, flushStatements need to be called to clear the cache after use.

  • ReuseExecutor

    It is a reusable actuator. The object of reuse is Statement, that is, the executor will cache the Statement of the same SQL to avoid repetitive creation of Statement. Its internal implementation is to maintain the Statement object through a HashMap. Since the current Map is only valid in this session, you need to call flushStatements to clear the Map after use.

  • CachingExecutor is a caching executor. It is only used when the second level cache is enabled.

4.1.2 StatementHandler

The StatementHandler object is responsible for setting the query parameters in the Statement object, processing the resultSet returned by JDBC, and processing the resultSet into a List collection to return.

Family members of StatementHandler:

  • StatementHandler is the interface;
  • BaseStatementHandler is an abstract class that implements StatementHandler, with some common methods built-in;
  • SimpleStatementHandler is responsible for processing Statement;
  • PreparedStatementHandler is responsible for processing PreparedStatement;
  • CallableStatementHandler is responsible for processing CallableStatement.
  • RoutingStatementHandler is responsible for proxying the specific subclasses of StatementHandler, and choose to instantiate SimpleStatementHandler, PreparedStatementHandler, CallableStatementHandler according to the Statement type.

4.1.3 ParameterHandler

ParameterHandler is responsible for converting incoming Java objects into JDBC type objects and filling values for the dynamic SQL of PreparedStatement.

ParameterHandler has only one concrete implementation class, namely DefaultParameterHandler.

4.1.4 ResultSetHandler

ResultSetHandler is responsible for two things:

  • Process the result set generated after the Statement is executed, and generate the result list
  • Processing the output parameters after the execution of the stored procedure

ResultSetHandler has only one concrete implementation class, namely DefaultResultSetHandler.

4.1.5 TypeHandler

TypeHandler is responsible for converting between Java object types and JDBC types.

4.2 SqlSession and Mapper

Let me first recall the code in the test program part of the complete example chapter of MyBatis.

Code snippet in the MyBatisDemo.java file:

// 2. 创建一个 SqlSession 实例,进行数据库操作
SqlSession sqlSession = factory.openSession();

// 3. Mapper 映射并执行
Long params = 1L;
List<User> list = sqlSession.selectList("io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey", params);
for (User user : list) {
    System.out.println("user name: " + user.getName());
}

In the sample code, pass the Statement Id and parameters of a configured Sql statement to the sqlSession object, and then return the result io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey is the Statement ID configured in UserMapper.xml, and params is SQL parameters.

Code snippet in UserMapper.xml file:

 <select id="selectByPrimaryKey" parameterType="java.lang.Long" resultMap="BaseResultMap">
    select id, name, age, address, email
    from user
    where id = #{id,jdbcType=BIGINT}
  </select>

MyBatis maps SqlSession and Mapper to each other through the fully qualified name of the method.

4.3. SqlSession and Executor

The source code of the selectList method in org.apache.ibatis.session.defaults.DefaultSqlSession:

@Override
public <E> List<E> selectList(String statement) {
  return this.selectList(statement, null);
}

@Override
public <E> List<E> selectList(String statement, Object parameter) {
  return this.selectList(statement, parameter, RowBounds.DEFAULT);
}

@Override
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
  try {
    // 1. 根据 Statement Id,在配置对象 Configuration 中查找和配置文件相对应的 MappedStatement
    MappedStatement ms = configuration.getMappedStatement(statement);
    // 2. 将 SQL 语句交由执行器 Executor 处理
    return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
  } finally {
    ErrorContext.instance().reset();
  }
}

description:

All configuration information of MyBatis is maintained in the Configuration object. A Map<String, MappedStatement> object is maintained in. Among them, key is the fully qualified name of the Mapper method (for this example, the key is io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey ), and the value is the MappedStatement object. Therefore, by passing in the Statement Id, the corresponding MappedStatement can be found from the Map.

MappedStatement maintains the metadata information of a Mapper method. For data organization, please refer to the debug screenshot below:

Summary: through the "SqlSession and Mapper" and "SqlSession and Executor" two sections, we already know: SqlSession's function is: according to the Statement ID, obtain the corresponding MappedStatement object in the Configuration, and then call Executor to execute the specific operate.

4.4. Executor workflow

Continuing the process in the previous section, SqlSession will hand over the SQL statement to the Executor for processing. So what did you do?

(1) Entrance of executor query

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    // 1. 根据传参,动态生成需要执行的 SQL 语句,用 BoundSql 对象表示
    BoundSql boundSql = ms.getBoundSql(parameter);
    // 2. 根据传参,创建一个缓存Key
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

The executor query entry mainly does two things:

  • Generate dynamic SQL: dynamically generate SQL statements that need to be executed according to the passed parameters, which are represented by BoundSql objects.
  • Manage cache: Create a cache key based on the passed parameters.

(2) The executor query the second entry

@SuppressWarnings("unchecked")
  @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    // 略
    List<E> list;
    try {
      queryStack++;
      list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
      // 3. 缓存中有值,则直接从缓存中取数据;否则,查询数据库
      if (list != null) {
        handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
      } else {
        list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
      }
    } finally {
      queryStack--;
    }
    // 略
    return list;
  }

The main function of the actual query method is to determine whether the cache key can hit the cache:

  • If it hits, the data in the cache will be returned;
  • If it is not hit, query the database:

(3) Query the database

  private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    List<E> list;
    localCache.putObject(key, EXECUTION_PLACEHOLDER);
    try {
      // 4. 执行查询,获取 List 结果,并将查询的结果更新本地缓存中
      list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
    } finally {
      localCache.removeObject(key);
    }
    localCache.putObject(key, list);
    if (ms.getStatementType() == StatementType.CALLABLE) {
      localOutputParameterCache.putObject(key, parameter);
    }
    return list;
  }

The responsibility of the queryFromDatabase method is to call doQuery, initiate a query to the database, and update the returned result to the local cache.

(4) Actual query method. Implementation of the doQuery() method of the SimpleExecutor class;

   @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();
      // 5. 根据既有的参数,创建StatementHandler对象来执行查询操作
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      // 6. 创建java.Sql.Statement对象,传递给StatementHandler对象
      stmt = prepareStatement(handler, ms.getStatementLog());
      // 7. 调用StatementHandler.query()方法,返回List结果
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

The above-mentioned Executor.query() method has undergone several twists and turns. Finally, a StatementHandler object will be created, and then the necessary parameters will be passed to the StatementHandler. The StatementHandler will be used to complete the query to the database and finally return the List result set. From the above code, we can see that the functions and functions of Executor are:

  • According to the passed parameters, complete the dynamic analysis of the SQL statement, and generate a BoundSql object for the StatementHandler to use;
  • Create a cache for queries to improve performance
  • Create a JDBC Statement connection object, pass it to the StatementHandler object, and return the List query result.

Implementation of prepareStatement() method:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    //对创建的Statement对象设置参数,即设置SQL 语句中 ? 设置为指定的参数
    handler.parameterize(stmt);
    return stmt;
  }

For the JDBC PreparedStatement type object, during the creation process, we use the SQL statement string will contain several placeholders, and then we will set the placeholders.

4.5. StatementHandler workflow

StatementHandler has a subclass of RoutingStatementHandler, which is responsible for proxying the work of other StatementHandler subclasses.

It will choose to instantiate the corresponding StatementHandler according to the configured Statement type, and then its proxy object will complete the work.

[Source code] RoutingStatementHandler

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

  switch (ms.getStatementType()) {
    case STATEMENT:
      delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case PREPARED:
      delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    case CALLABLE:
      delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
      break;
    default:
      throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
  }

}

[Source code] The parameterize method source code of RoutingStatementHandler

[Source code] Source code of parameterize method of PreparedStatementHandler

StatementHandler uses the ParameterHandler object to complete the assignment of Statement.

@Override
public void parameterize(Statement statement) throws SQLException {
  // 使用 ParameterHandler 对象来完成对 Statement 的设值
  parameterHandler.setParameters((PreparedStatement) statement);
}

[Source code] The query method source code of StatementHandler

StatementHandler uses the ResultSetHandler object to complete the processing of the ResultSet.

@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
  PreparedStatement ps = (PreparedStatement) statement;
  ps.execute();
  // 使用ResultHandler来处理ResultSet
  return resultSetHandler.handleResultSets(ps);
}

4.6. ParameterHandler workflow

[Source code] SetParameters method of DefaultParameterHandler

@Override
  public void setParameters(PreparedStatement ps) {
    // parameterMappings 是对占位符 #{} 对应参数的封装
    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 {
            // 获取对象中相应的属性或查找 Map 对象中的值
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            value = metaObject.getValue(propertyName);
          }

          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            // 通过 TypeHandler 将 Java 对象参数转为 JDBC 类型的参数
            // 然后,将数值动态绑定到 PreparedStaement 中
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

4.7. ResultSetHandler workflow

The implementation of ResultSetHandler can be summarized as follows: Convert the result set after Statement execution into the corresponding JavaBean object according to the ResultType or ResultMap configured in the Mapper file, and finally return the result.

[Source code] The handleResultSets method of DefaultResultSetHandler. The handleResultSets method is the most critical method of DefaultResultSetHandler. Its implementation is as follows:

@Override
public List<Object> handleResultSets(Statement stmt) throws SQLException {
  ErrorContext.instance().activity("handling results").object(mappedStatement.getId());

  final List<Object> multipleResults = new ArrayList<>();

  int resultSetCount = 0;
  // 第一个结果集
  ResultSetWrapper rsw = getFirstResultSet(stmt);
  List<ResultMap> resultMaps = mappedStatement.getResultMaps();
  // 判断结果集的数量
  int resultMapCount = resultMaps.size();
  validateResultMapsCount(rsw, resultMapCount);
  // 遍历处理结果集
  while (rsw != null && resultMapCount > resultSetCount) {
    ResultMap resultMap = resultMaps.get(resultSetCount);
    handleResultSet(rsw, resultMap, multipleResults, null);
    rsw = getNextResultSet(stmt);
    cleanUpAfterHandlingResultSet();
    resultSetCount++;
  }

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

Five, reference materials

official

  1. MyBatis Github
  2. MyBatis official website
  3. MyBatis Generator
  4. Spring integration
  5. Spring Boot integration

extension plug-in

  1. MyBatis-plus -CRUD extension plug-in, code generator, pager and other functions
  2. Mapper -CRUD extension
  3. MyBatis-PageHelper -MyBatis universal paging plugin

article

  1. " depth understanding of the principle of MyBatis "
  2. " MyBatis source code Chinese annotation "
  3. " powerful resultMap MyBatis"
Author: vivo internet server team-Zhang Peng

vivo互联网技术
3.3k 声望10.2k 粉丝