关于mybatis:从源码角度分析-MyBatis-工作原理

5次阅读

共计 21500 个字符,预计需要花费 54 分钟才能阅读完成。

一、MyBatis 残缺示例

这里,我将以一个入门级的示例来演示 MyBatis 是如何工作的。

注:本文前面章节中的原理、源码局部也将基于这个示例来进行解说。残缺示例源码地址

1.1. 数据库筹备

在本示例中,须要针对一张用户表进行 CRUD 操作。其数据模型如下:

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. 增加 MyBatis

如果应用 Maven 来构建我的项目,则需将上面的依赖代码置于 pom.xml 文件中:

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

1.3. MyBatis 配置

XML 配置文件中蕴含了对 MyBatis 零碎的外围设置,包含获取数据库连贯实例的数据源(DataSource)以及决定事务作用域和管制形式的事务管理器(TransactionManager)。

本示例中只是给出最简化的配置。【示例】MyBatis-config.xml 文件

<?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>

阐明:下面的配置文件中仅仅指定了数据源连贯形式和 User 表的映射配置文件。

1.4 Mapper

1.4.1 Mapper.xml

集体了解,Mapper.xml 文件能够看做是 MyBatis 的 JDBC SQL 模板。【示例】UserMapper.xml 文件。

上面是一个通过 MyBatis Generator 主动生成的残缺的 Mapper 文件。

<?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

Mapper.java 文件是 Mapper.xml 对应的 Java 对象。【示例】UserMapper.java 文件

public interface UserMapper {int deleteByPrimaryKey(Long id);

    int insert(User record);

    User selectByPrimaryKey(Long id);

    List<User> selectAll();

    int updateByPrimaryKey(User record);

}

比照 UserMapper.java 和 UserMapper.xml 文件,不难发现:UserMapper.java 中的办法和 UserMapper.xml 的 CRUD 语句元素(<insert>、<delete>、<update>、<select>)存在一一对应关系。

在 MyBatis 中,正是通过办法的全限定名,将二者绑定在一起。

1.4.3 数据实体.java

【示例】User.java 文件

public class User {
    private Long id;

    private String name;

    private Integer age;

    private String address;

    private String email;

}

<insert>、<delete>、<update>、<select> 的 parameterType 属性以及 <resultMap> 的 type 属性都可能会绑定到数据实体。这样就能够把 JDBC 操作的输入输出和 JavaBean 联合起来,更加不便、易于了解。

1.5. 测试程序

【示例】MyBatisDemo.java 文件

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: 张三
    }

}

阐明:SqlSession 接口是 MyBatis API 外围中的外围,它代表 MyBatis 和数据库一次残缺会话。

  • MyBatis 会解析配置,并依据配置创立 SqlSession。
  • 而后,MyBatis 将 Mapper 映射为 SqlSession,而后传递参数,执行 SQL 语句并获取后果。

二、MyBatis 生命周期

2.1. SqlSessionFactoryBuilder

2.1.1 SqlSessionFactoryBuilder 的职责

SqlSessionFactoryBuilder 负责创立 SqlSessionFactory 实例。

SqlSessionFactoryBuilder 能够从 XML 配置文件或一个事后定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。

Configuration 类蕴含了对一个 SqlSessionFactory 实例你可能关怀的所有内容。

SqlSessionFactoryBuilder 利用了建造者设计模式,它有五个 build 办法,容许你通过不同的资源创立 SqlSessionFactory 实例。

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 SqlSessionFactoryBuilder 的生命周期

SqlSessionFactoryBuilder 能够被实例化、应用和抛弃,一旦创立了 SqlSessionFactory,就不再须要它了。因而 SqlSessionFactoryBuilder 实例的最佳作用域是办法作用域(也就是部分办法变量)。

你能够重用 SqlSessionFactoryBuilder 来创立多个 SqlSessionFactory 实例,但最好还是不要始终保留着它,以保障所有的 XML 解析资源能够被开释给更重要的事件。

2.2. SqlSessionFactory

2.2.1 SqlSessionFactory 职责

SqlSessionFactory 负责创立 SqlSession 实例。

SqlSessionFactory 利用了工厂设计模式,它提供了一组办法,用于创立 SqlSession 实例。

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

办法阐明:

默认的 openSession() 办法没有参数,它会创立具备如下个性的 SqlSession:

1)事务作用域将会开启(也就是不主动提交)。

  • 将由以后环境配置的 DataSource 实例中获取 Connection 对象。
  • 事务隔离级别将会应用驱动或数据源的默认设置。
  • 预处理语句不会被复用,也不会批量解决更新。

2)TransactionIsolationLevel 示意事务隔离级别,它对应着 JDBC 的五个事务隔离级别。

3)ExecutorType 枚举类型定义了三个值:

  • ExecutorType.SIMPLE:该类型的执行器没有特地的行为。它为每个语句的执行创立一个新的预处理语句。
  • ExecutorType.REUSE:该类型的执行器会复用预处理语句。
  • ExecutorType.BATCH:该类型的执行器会批量执行所有更新语句,如果 SELECT 在多个更新两头执行,将在必要时将多条更新语句分隔开来,以不便了解。

2.2.2 SqlSessionFactory 生命周期

SQLSessionFactory 应该以单例模式在利用的运行期间始终存在。

2.3. SqlSession

2.3.1 SqlSession 职责

MyBatis 的次要 Java 接口就是 SqlSession。它蕴含了所有执行语句,获取映射器和治理事务等办法。具体内容能够参考:「MyBatis 官网文档之 SqlSessions」。

SQLSession 类的办法可依照下图进行大抵分类:

2.3.2 SqlSession 生命周期

SqlSessions 是由 SqlSessionFactory 实例创立的;而 SqlSessionFactory 是由 SqlSessionFactoryBuilder 创立的。

🔔 留神:当 MyBatis 与一些依赖注入框架(如 Spring 或者 Guice)同时应用时,SqlSessions 将被依赖注入框架所创立,所以你不须要应用 SqlSessionFactoryBuilder 或者 SqlSessionFactory。

每个线程都应该有它本人的 SqlSession 实例。

SqlSession 的实例不是线程平安的,因而是不能被共享的,所以它的最佳的作用域是申请或办法作用域。相对不能将 SqlSession 实例的援用放在一个类的动态域,甚至一个类的实例变量也不行。也绝不能将 SqlSession 实例的援用放在任何类型的托管作用域中,比方 Servlet 框架中的 HttpSession。正确在 Web 中应用 SqlSession 的场景是:每次收到的 HTTP 申请,就能够关上一个 SqlSession,返回一个响应,就敞开它。

编程模式:

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

2.4. 映射器

2.4.1 映射器职责

映射器是一些由用户创立的、绑定 SQL 语句的接口。

SqlSession 中的 insert、update、delete 和 select 办法都很弱小,但也有些繁琐。更通用的形式是应用映射器类来执行映射语句。一个映射器类就是一个仅需申明与 SqlSession 办法相匹配的办法的接口类。

MyBatis 将配置文件中的每一个 <mapper> 节点形象为一个 Mapper 接口,而这个接口中申明的办法和跟 <mapper> 节点中的 <select|update|delete|insert> 节点绝对应,即 <select|update|delete|insert> 节点的 id 值为 Mapper 接口中的办法名称,parameterType 值示意 Mapper 对应办法的入参类型,而 resultMap 值则对应了 Mapper 接口示意的返回值类型或者返回后果集的元素类型。

MyBatis 会依据相应的接口申明的办法信息,通过动静代理机制生成一个 Mapper 实例;MyBatis 会依据这个办法的办法名和参数类型,确定 Statement id,而后和 SqlSession 进行映射,底层还是通过 SqlSession 实现和数据库的交互。

上面的示例展现了一些办法签名以及它们是如何映射到 SqlSession 上的。

留神:

  • 映射器接口不须要去实现任何接口或继承自任何类。只有办法能够被惟一标识对应的映射语句就能够了。
  • 映射器接口能够继承自其余接口。当应用 XML 来构建映射器接口时要保障语句被蕴含在适合的命名空间中。而且,惟一的限度就是你不能在两个继承关系的接口中领有雷同的办法签名(潜在的危险做法不可取)。

2.4.2 映射器生命周期

映射器接口的实例是从 SqlSession 中取得的。因而从技术层面讲,任何映射器实例的最大作用域是和申请它们的 SqlSession 雷同的。尽管如此,映射器实例的最佳作用域是办法作用域。也就是说,映射器实例应该在调用它们的办法中被申请,用过之后即可抛弃。

编程模式:

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

映射器注解

MyBatis 是一个 XML 驱动的框架。配置信息是基于 XML 的,而且映射语句也是定义在 XML 中的。MyBatis 3 当前,反对注解配置。注解配置基于配置 API;而配置 API 基于 XML 配置。

MyBatis 反对诸如 @Insert、@Update、@Delete、@Select、@Result 等注解。

具体内容请参考:MyBatis 官网文档之 sqlSessions,其中列举了 MyBatis 反对的注解清单,以及根本用法。

三、MyBatis 的架构

从 MyBatis 代码实现的角度来看,MyBatis 的次要组件有以下几个:

  • SqlSession – 作为 MyBatis 工作的次要顶层 API,示意和数据库交互的会话,实现必要数据库增删改查性能。
  • Executor – MyBatis 执行器,是 MyBatis 调度的外围,负责 SQL 语句的生成和查问缓存的保护。
  • StatementHandler – 封装了 JDBC Statement 操作,负责对 JDBC statement 的操作,如设置参数、将 Statement 后果集转换成 List 汇合。
  • ParameterHandler – 负责对用户传递的参数转换成 JDBC Statement 所须要的参数。
  • ResultSetHandler – 负责将 JDBC 返回的 ResultSet 后果集对象转换成 List 类型的汇合。
  • TypeHandler – 负责 java 数据类型和 jdbc 数据类型之间的映射和转换。
  • MappedStatement – MappedStatement 保护了一条 <select|update|delete|insert> 节点的封装。
  • SqlSource – 负责依据用户传递的 parameterObject,动静地生成 SQL 语句,将信息封装到 BoundSql 对象中,并返回。
  • BoundSql – 示意动静生成的 SQL 语句以及相应的参数信息。
  • Configuration – MyBatis 所有的配置信息都维持在 Configuration 对象之中。

这些组件的架构档次如下:

3.1. 配置层

配置层决定了 MyBatis 的工作形式。

MyBatis 提供了两种配置形式:

  • 基于 XML 配置文件的形式
  • 基于 Java API 的形式

SqlSessionFactoryBuilder 会依据配置创立 SqlSessionFactory;

SqlSessionFactory 负责创立 SqlSessions。

3.2. 接口层

接口层负责和数据库交互的形式。MyBatis 和数据库的交互有两种形式:

1)应用 SqlSession:SqlSession 封装了所有执行语句,获取映射器和治理事务的办法。

  • 用户只须要传入 Statement Id 和查问参数给 SqlSession 对象,就能够很不便的和数据库进行交互。
  • 这种形式的毛病是不合乎面向对象编程的范式。

2)应用 Mapper 接口:MyBatis 会依据相应的接口申明的办法信息,通过动静代理机制生成一个 Mapper 实例;MyBatis 会依据这个办法的办法名和参数类型,确定 Statement Id,而后和 SqlSession 进行映射,底层还是通过 SqlSession 实现和数据库的交互。

3.3. 数据处理层

数据处理层能够说是 MyBatis 的外围,从大的方面上讲,它要实现两个性能:

1)依据传参 Statement 和参数构建动静 SQL 语句

  • 动静语句生成能够说是 MyBatis 框架十分优雅的一个设计,MyBatis 通过传入的参数值,应用 Ognl 来动静地结构 SQL 语句,使得 MyBatis 有很强的灵活性和扩展性。
  • 参数映射指的是对于 java 数据类型和 jdbc 数据类型之间的转换:这里有包含两个过程:查问阶段,咱们要将 java 类型的数据,转换成 jdbc 类型的数据,通过 preparedStatement.setXXX() 来设值;另一个就是对 resultset 查问后果集的 jdbcType 数据转换成 java 数据类型。

2)执行 SQL 语句以及解决响应后果集 ResultSet

  • 动静 SQL 语句生成之后,MyBatis 将执行 SQL 语句,并将可能返回的后果集转换成 List<E> 列表。
  • MyBatis 在对后果集的解决中,反对后果集关系一对多和多对一的转换,并且有两种反对形式,一种为嵌套查问语句的查问,还有一种是嵌套后果集的查问。

3.4. 框架撑持层

1) 事务管理机制 – MyBatis 将事务形象成了 Transaction 接口。MyBatis 的事务管理分为两种模式:

  • 应用 JDBC 的事务管理机制:即利用 java.sql.Connection 对象实现对事务的提交(commit)、回滚(rollback)、敞开(close)等。
  • 应用 MANAGED 的事务管理机制:MyBatis 本身不会去实现事务管理,而是让程序的容器如(JBOSS,Weblogic)来实现对事务的治理。

2) 连接池治理

3) SQL 语句的配置 – 反对两种形式:

  • xml 配置
  • 注解配置

4) 缓存机制 – MyBatis 采纳两级缓存构造;

  • 一级缓存 是 Session 会话级别的缓存 – 一级缓存又被称之为本地缓存。一般而言,一个 SqlSession 对象会应用一个 Executor 对象来实现会话操作,Executor 对象会保护一个 Cache 缓存,以进步查问性能。
  1. 一级缓存的生命周期是 Session 会话级别的。
  • 二级缓存 是 Application 利用级别的缓存 – 用户配置了 “cacheEnabled=true”,才会开启二级缓存。
  1. 如果开启了二级缓存,SqlSession 会先应用 CachingExecutor 对象来解决查问申请。CachingExecutor 会在二级缓存中查看是否有匹配的数据,如果匹配,则间接返回缓存后果;如果缓存中没有,再交给真正的 Executor 对象来实现查问,之后 CachingExecutor 会将真正 Executor 返回的查问后果搁置到缓存中,而后在返回给用户。
  2. 二级缓存的生命周期是利用级别的。

四、SqlSession 外部工作机制

从前文,咱们曾经理解了,MyBatis 封装了对数据库的拜访,把对数据库的会话和事务管制放到了 SqlSession 对象中。那么具体是如何工作的呢?接下来,咱们通过源码解读来进行剖析。

SqlSession 对于 insert、update、delete、select 的外部解决机制基本上大同小异。所以,接下来,我会以一次残缺的 select 查问流程为例解说 SqlSession 外部的工作机制。置信读者如果了解了 select 的解决流程,对于其余 CRUD 操作也能做到一通百通。

4.1  SqlSession 子组件

后面的内容曾经介绍了:SqlSession 是 MyBatis 的顶层接口,它提供了所有执行语句,获取映射器和治理事务等办法。

实际上,SqlSession 是通过聚合多个子组件,让每个子组件负责各自性能的形式,实现了工作的下发。

在理解各个子组件工作机制前,先让咱们简略认识一下 SqlSession 的外围子组件。

4.1.1 Executor

Executor 即执行器,它负责生成动静 SQL 以及治理缓存。

  • Executor 即执行器接口。
  • BaseExecutor

    是 Executor 的抽象类,它采纳了模板办法设计模式,内置了一些共性办法,而将定制化办法留给子类去实现。

  • SimpleExecutor

    是最简略的执行器。它只会间接执行 SQL,不会做额定的事。

  • BatchExecutor

    是批处理执行器。它的作用是通过批处理来优化性能。值得注意的是,批量更新操作,因为外部有缓存机制,应用完后须要调用 flushStatements 来革除缓存。

  • ReuseExecutor

    是可重用的执行器。重用的对象是 Statement,也就是说,该执行器会缓存同一个 SQL 的 Statement,防止反复创立 Statement。其外部的实现是通过一个 HashMap 来保护 Statement 对象的。因为以后 Map 只在该 session 中无效,所以应用完后须要调用 flushStatements 来革除 Map。

  • CachingExecutor 是缓存执行器。它只在启用二级缓存时才会用到。

4.1.2 StatementHandler

StatementHandler 对象负责设置 Statement 对象中的查问参数、解决 JDBC 返回的 resultSet,将 resultSet 加工为 List 汇合返回。

StatementHandler 的家族成员:

  • StatementHandler 是接口;
  • BaseStatementHandler 是实现 StatementHandler 的抽象类,内置一些共性办法;
  • SimpleStatementHandler 负责解决 Statement;
  • PreparedStatementHandler 负责解决 PreparedStatement;
  • CallableStatementHandler 负责解决 CallableStatement。
  • RoutingStatementHandler 负责代理 StatementHandler 具体子类,依据 Statement 类型,抉择实例化 SimpleStatementHandler、PreparedStatementHandler、CallableStatementHandler。

4.1.3 ParameterHandler

ParameterHandler 负责将传入的 Java 对象转换 JDBC 类型对象,并为 PreparedStatement 的动静 SQL 填充数值。

ParameterHandler 只有一个具体实现类,即 DefaultParameterHandler。

4.1.4 ResultSetHandler

ResultSetHandler 负责两件事:

  • 解决 Statement 执行后产生的后果集,生成后果列表
  • 解决存储过程执行后的输入参数

ResultSetHandler 只有一个具体实现类,即 DefaultResultSetHandler。

4.1.5 TypeHandler

TypeHandler 负责将 Java 对象类型和 JDBC 类型进行互相转换。

4.2  SqlSession 和 Mapper

先来回顾一下 MyBatis 残缺示例章节的 测试程序局部的代码。

MyBatisDemo.java 文件中的代码片段:

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

示例代码中,给 sqlSession 对象的传递一个配置的 Sql 语句的 Statement Id 和参数,而后返回后果 io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey 是配置在 UserMapper.xml 的 Statement ID,params 是 SQL 参数。

UserMapper.xml 文件中的代码片段:

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

MyBatis 通过办法的全限定名,将 SqlSession 和 Mapper 互相映射起来。

4.3. SqlSession 和 Executor

org.apache.ibatis.session.defaults.DefaultSqlSession 中 selectList 办法的源码:

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

阐明:

MyBatis 所有的配置信息都维持在 Configuration 对象之中。中保护了一个 Map<String, MappedStatement> 对象。其中,key 为 Mapper 办法的全限定名(对于本例而言,key 就是 io.github.dunwu.spring.orm.mapper.UserMapper.selectByPrimaryKey),value 为 MappedStatement 对象。所以,传入 Statement Id 就能够从 Map 中找到对应的 MappedStatement。

MappedStatement 保护了一个 Mapper 办法的元数据信息,数据组织能够参考上面 debug 截图:

小结:通过 “SqlSession 和 Mapper” 以及 “SqlSession 和 Executor” 这两节,咱们曾经晓得:SqlSession 的职能是:依据 Statement ID, 在 Configuration 中获取到对应的 MappedStatement 对象,而后调用 Executor 来执行具体的操作。

4.4. Executor 工作流程

持续上一节的流程,SqlSession 将 SQL 语句交由执行器 Executor 解决。那又做了哪些事呢?

(1)执行器查问入口

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

执行器查问入口次要做两件事:

  • 生成动静 SQL:依据传参,动静生成须要执行的 SQL 语句,用 BoundSql 对象示意。
  • 治理缓存:依据传参,创立一个缓存 Key。

(2)执行器查问第二入口

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

理论查询方法次要的职能是判断缓存 key 是否能命中缓存:

  • 命中,则将缓存中数据返回;
  • 不命中,则查询数据库:

(3)查询数据库

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

queryFromDatabase 办法的职责是调用 doQuery,向数据库发动查问,并将返回的后果更新到本地缓存。

(4)理论查询方法。SimpleExecutor 类的 doQuery()办法实现;

   @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {Configuration configuration = ms.getConfiguration();
      // 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);
    }
  }

上述的 Executor.query()办法几经转折,最初会创立一个 StatementHandler 对象,而后将必要的参数传递给 StatementHandler,应用 StatementHandler 来实现对数据库的查问,最终返回 List 后果集。从下面的代码中咱们能够看出,Executor 的性能和作用是:

  • 依据传递的参数,实现 SQL 语句的动静解析,生成 BoundSql 对象,供 StatementHandler 应用;
  • 为查问创立缓存,以进步性能
  • 创立 JDBC 的 Statement 连贯对象,传递给 StatementHandler 对象,返回 List 查问后果。

prepareStatement() 办法的实现:

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

对于 JDBC 的 PreparedStatement 类型的对象,创立的过程中,咱们应用的是 SQL 语句字符串会蕴含若干个占位符,咱们其后再对占位符进行设值。

4.5. StatementHandler 工作流程

StatementHandler 有一个子类 RoutingStatementHandler,它负责代理其余 StatementHandler 子类的工作。

它会依据配置的 Statement 类型,抉择实例化相应的 StatementHandler,而后由其代理对象实现工作。

【源码】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());
  }

}

【源码】RoutingStatementHandler 的 parameterize 办法源码

【源码】PreparedStatementHandler 的 parameterize 办法源码

StatementHandler 应用 ParameterHandler 对象来实现对 Statement 的赋值。

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

【源码】StatementHandler 的 query 办法源码

StatementHandler 应用 ResultSetHandler 对象来实现对 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 工作流程

【源码】DefaultParameterHandler 的 setParameters 办法

@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 工作流程

ResultSetHandler 的实现能够概括为:将 Statement 执行后的后果集,依照 Mapper 文件中配置的 ResultType 或 ResultMap 来转换成对应的 JavaBean 对象,最初将后果返回。

【源码】DefaultResultSetHandler 的 handleResultSets 办法。handleResultSets 办法是 DefaultResultSetHandler 的最要害办法。其实现如下:

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

五、参考资料

官网

  1. MyBatis Github
  2. MyBatis 官网
  3. MyBatis Generator
  4. Spring 集成
  5. Spring Boot 集成

扩大插件

  1. MyBatis-plus – CRUD 扩大插件、代码生成器、分页器等多功能
  2. Mapper – CRUD 扩大插件
  3. MyBatis-PageHelper – MyBatis 通用分页插件

文章

  1. 《深刻了解 MyBatis 原理》
  2. 《MyBatis 源码中文正文》
  3. 《MyBatis 中弱小的 resultMap》

作者:vivo 互联网服务器团队 -Zhang Peng

正文完
 0