Mybatis(一) - 概述

1. MyBatis的整体架构

首先小册先来讲解一下 MyBatis 的整体架构,这里面涉及到的内容可能小伙伴在刚开始学习 MyBatis 的时候就接触过了,不过由于是初学,可能当时理解起来不是那么直接,也可能没有完全的吃透,所以小册先来带各位回顾一下 MyBatis 的整体架构。

下面这张图,很多小伙伴看着都很眼熟吧:

img

自上而下我们一步一步来说:

  1. 从 MyBatis 解析全局配置文件开始,它会将其中定义好的 mapper.xmlMapper 接口一并加载,并统一保存到全局 Configuration 配置对象中;

  2. SqlSessionFactory 的构建,需要全局的 Configuration 配置,而 SqlSessionFactory 可以创建出 SqlSession 供我们与 MyBatis 交互;

  3. SqlSession 在执行时,底层是通过一个 Executor ,根据要执行的 SQL Id (即 statementId ),找到对应的 MappedStatement ,并根据输入的参数组装 SQL ;

  4. MappedStatement 组装好 SQL 后,底层会操作原生 jdbc 的 API ,去数据库执行 SQL ,如果是查询的话,会返回查询结果集 ResultSet

  5. MyBatis 拿到 ResultSet 后,由 ResultHandler 负责封装结果集,根据我们事先定义好的结果集类型,封装好结果集后返回。

这里面除了 SqlSessionFactorySqlSession 之外,有两个相当重要的类需要我们前置了解,之前的章节我们只是遇到它们时简单的提一下,但现在我们要掌握全局的生命周期,就必须对它们也深入了解。下面我们来详细了解一下它们。

2. Executor

Executor ,字面意为 “执行器” ,它在整个 MyBatis 的执行流程中起到了 “枢纽” 的角色,SqlSession 的所有 SQL 执行,底层都是委托 Executor 执行,而 Executor 又是直接跟底层 jdbc 的 API 打交道的,所以我们一定要搞明白这个 Executor 的设计。

2.1 结构设计

Executor 本身是一个接口,它里面定义了好多方法,我们挑几个比较重要的方法先来看一下:

可以发现,Executor 中真的把需要与数据库交互的操作,基本都定义好了,这里面大体包含这么多吧:

  • CRUD 操作。

  • 事务控制和获取。

  • 二级缓存的控制。

  • 延迟加载等。

不过这也仅仅是定义的接口方法,下面肯定还有落地实现,Executor 本身有两个直接的抽象实现类,共 5 个最终落地实现类:

img

下面我们挑其中重要的实现类来讲解。

2.2 基础子类:BaseExecutor

Base 开头,很明显它就是个抽象的父类,不过这个 BaseExecutor 实现的倒是不少,大多都是逻辑骨架之类的代码实现,我们可以展开聊聊。

2.2.1 重要成员

这个类的成员有不少重要的,而且不乏一些我们之前看到过的:

注释我们都标注好了,其中有几个重要的或者容易引起好奇心的,小册可以多说两嘴:

  • Executor wrapperExecutor 本身也有装饰者的设计(比方说普通的 Executor 可以通过套一层装饰者,实现二级缓存的预检查和存储);

  • PerpetualCache localOutputParameterCache :这个东西跟底层操作 jdbc 中 Statement 的类型有关,一般情况下我们不会接触到它,所以不用管了;

  • queryStack :当遇到嵌套查询时,这个计数器可以及时的记录,并在合适的位置控制检查是否需要清空缓存。

简单了解一下就可以哈,下面遇到它们的时候,小册还会说的。

下面我们来看看 BaseExecutor 都提供了哪些基础的方法。

2.2.2 query

论基础且最重要的方法,那必属于我 CRUD 四大金刚了。我们先看查询的 query 方法吧:

乍一看,这个 query 方法很简单啊,不要着急,这个方法只是个中转站,这里它主要的工作,是获取到要发送的 SQL ,以及根据要去往数据库的查询请求,构造出缓存的标识(即 CacheKey ),之后继续往下传。

下面重载的 query 方法才是重头戏,小册已经把整体流程都标注了注释,各位先整体扫一遍流程,理一下脉络:

扫一遍下来,我们可以很清楚的从这段源码中获得这样的思路:

  1. 查询之前计数,并清空一级缓存(有必要的话)

  2. 先不着急去数据库里查,先看看缓存里有没有

    • 如果有,直接取缓存

    • 没有,就查数据库

  1. 查完了处理后续工作

OK ,有这个思路就够了,具体的方法我们没有必要现在就着急深入探索,后面我们跑全流程的时候自然会深入的。

2.2.3 update

除了 query 之外,另一个重要的 CRUD 方法就是 update 了,我们都知道,insert update delete 语句都可以使用 update 的动作完成,所以 Executor 也只设计了一个 update 方法完事,BaseExecutor 中的 update 方法倒是简单:

这很明显是模板方法的套路啊,doUpdate 才是真正干活的吧!我们往下翻一下:

嗯,果然不出我们所料 ~ 具体 doUpdate 都怎么实现,我们现在也是不着急深入哈,后面全流程走的时候自然会看到的。

哦对了,我们多留意一点,发现 doUpdate 方法是个模板方法的时候,下面居然还有个 doQuery 方法:

诶?这就奇了怪了,上面看 query 方法的时候,也没看到有 doQuery 方法啊,咋回事呢?如果小伙伴有这个疑问,我只能说一句你太棒了,你的主动思考能力就是驱使你去探索学习的源动力,请保持住这种感觉,加油 ~

至于 doQuery 方法搁哪儿调用的,我们同样也是跟 doUpdate 一块儿,后面再找。

2.2.4 其他方法

除了读和写的动作之外,Executor 中也负责事务的提交和回滚:

注意 Executor 不负责开启事务,因为事务在 Executor 的创建时期就已经传入进去了:

还有一些别的方法,我们后面走全流程的时候遇到再说吧,这里我们可以做到对 BaseExecutor 有一个整体的认识就可以了。

2.3 基本实现类:SimpleExecutor

下面我们来介绍 BaseExecutor 的一个重要的实现子类,它也是最简单最基础的实现类: SimpleExecutor,由于 BaseExecutor 中有设计一些模板方法,SimpleExecutor 就只是负责把这些方法实现了,没有别的多余的设计。

首先是 doQuery 方法,这基本就是直接操作 jdbc 的 API 了:

接下来是 doUpdate 方法,发现套路几乎是一样的,只是一个调用 StatementHandlerquery 方法,一个是调用 update 方法:

注意留意!这里面涉及到了之前在讲解 MyBatis 插件时说的 StatementHandler !之前我们提到过,MyBatis 的插件 / 拦截器,可以拦截 StatementHandler 的部分方法,其中就有 queryupdate 方法,这里我们要稍微留意一下哈。

2.4 带二级缓存的实现类:CachingExecutor

再然后,是可以体现出 MyBatis 二级缓存的 CachingExecutor 了,我们之前在二级缓存的章节中遇见过它,不过当时小册只是顺口提了一下这个类名(毕竟当时我们要关注的是二级缓存的生效原理),现在我们也简单看看它。

请注意,CachingExecutor 本身是一个装饰者,所以它并没有继承自 BaseExecutor (如果继承了,那就意味着 CachingExecutor 内部也会组合一个一级缓存,这显然不符合设计),而是只实现了 Executor 接口。装饰者,那内部一定要有一个 delegate

另外还有一个 TransactionalCacheManager ,至于为什么设计它,小册在第 15 章的 2.3.3 章节已经讲解过了,忘记的小伙伴可以返回去看。

除此之外,CachingExecutor 具体在执行过程中都怎么干活,我们也是放到后面的全流程执行中研究。

OK ,有关 Executor 的部分小册就讲这么多,下面是另一个同等重要的核心 API :**MappedStatement**

3. MappedStatement

Executor 同等重要的,还有它下游的 MappedStatement ,这个 Statement 的概念,就是我们从一开始学习 MyBatis 的时候提到的,mapper.xml 中写的一个一个的 <select> 也好,<insert> 等等也好,这些都会在底层封装为一个一个的 MappedStatement ,前面在解析 mapper.xml 和注解 Mapper 接口时我们也都看到了基本的脉络,本章我们重点关注它的结构。

3.1 重要成员

MappedStatement 本身是一个独立的类,没有继承也没有扩展,所以我们不用去分析继承结构体系,只需要看看这里面组合了哪些重要的成员就 OK 了。

不过有点头疼的是,MappedStatement 本身的成员属性实在是有点多,小册只捡一些重要的,咱们一起来看看就 OK 了:

可以发现,MappedStatement 中保存的其实就是 mapper.xml 或者注解 Mapper 接口中定义的那些元素的信息(有点元信息的味了),当然也有最最重要的 SQL 语句封装,注意它不是用一个普通的 String 去封装 SQL ,而是一个专门的 SqlSource ,为什么要设计它,我们马上就说。

3.2 SqlSource的设计

先解释一下为什么不用普通的 String 去封装 SQL 。

3.2.1 SqlSource的意义

其实原因很简单,比方说下面的一条 mapper 定义:

请问这条 SQL 要如何封装呢?动态 SQL 在实际查询的时候,应该是根据传入的参数,动态的组合 if 标签来生成 SQL ,所以用 String 来记录 SQL 是不现实的。

3.2.2 SqlSource的定义

SqlSource 本身是一个接口,它只有一个方法,就是获取 SQL :

注意这里它的返回值又不是 String ,而是一个 BoundSql ,看到这里可能小伙伴头顶上的问号越来越大了,这都图个啥啊???不要方,小册给你一个简单的图理解一下:

img

如图中所示,SqlSource 我们可以理解为带动态 SQL 标签的 SQL 定义 ,在程序运行期间,给 SqlSource 传入 SQL 必需的参数后,它会解析这些动态 SQL ,并生成一条真正可用于 PreparedStatement 的 SQL ,并且把这些参数也都保存好,而保存 SQL 和参数的载体就是 BoundSql 了。

当然,话又说回来,并不是所有的 SQL 定义都是动态的,也存在一些很简单的 SQL 呀(比方说 findById 这样的),这种情况下就没有必要用复杂 SQL 的模型组合了,所以 SqlSource 本身是有好几种实现的,下面我们来看它的一些实现。

img

3.2.3 简单实现:StaticSqlSource

静态的 SQL ,这个就是上面提到的,类似于 findAllfindById 的那种 SQL ,它们的 SQL 定义都没有动态标签,MyBatis 底层就会用这种方式封装。

StaticSqlSource 的设计相当简单,底层就是封装的明文 SQL :

3.2.4 动态SQL:DynamicSqlSource

动态的 SQL ,对应的就是那些用了动态 SQL 标签的 statement 了,它的设计就不像 StaticSqlSource 那么简单了,它的底层是一个 SqlNode

而且看下面 getBoundSql 方法的逻辑也比较奇怪,它又借助 DynamicContextSqlSourceBuilder 这两个东西来辅助生成 SQL ,这几个东西又是干什么的呢?这里我们先不展开了,后面我们会讲解这里面的每一步动作。

3.2.5 基于Provider:ProviderSqlSource

还记得第 12 章我们有讲 Provider 系列的注解吗,使用 Provider 的方式定义的 statement ,MyBatis 会选用这种 SqlSource 封装。它的底层是一系列反射的元素:

这种方式我们平时用的不多,MyBatis 本身也不是很喜欢我们使用这种方式,所以小册也不展开研究了,感兴趣的小伙伴可以自行研究一下。

3.3 MappedStatement的重要方法

好了话说回来,MappedStatement 的内部核心有 SQL 的定义,也有这些 statement 对应的一些配置元信息的存储,除此之外,它还有一个重要的方法,就是直接从 MappedStatement 上解析 SQL :

说是重要,其实也没有多重要,因为它内部就是转发了一层而已,实际干活的还是上面提到的 SqlSource

最后更新于

这有帮助吗?