1. MyBatis的整体架构
首先小册先来讲解一下 MyBatis 的整体架构,这里面涉及到的内容可能小伙伴在刚开始学习 MyBatis 的时候就接触过了,不过由于是初学,可能当时理解起来不是那么直接,也可能没有完全的吃透,所以小册先来带各位回顾一下 MyBatis 的整体架构。
下面这张图,很多小伙伴看着都很眼熟吧:
自上而下我们一步一步来说:
从 MyBatis 解析全局配置文件开始,它会将其中定义好的 mapper.xml
、Mapper
接口一并加载,并统一保存到全局 Configuration
配置对象中;
SqlSessionFactory
的构建,需要全局的 Configuration
配置,而 SqlSessionFactory
可以创建出 SqlSession
供我们与 MyBatis 交互;
SqlSession
在执行时,底层是通过一个 Executor
,根据要执行的 SQL Id (即 statementId
),找到对应的 MappedStatement
,并根据输入的参数组装 SQL ;
MappedStatement
组装好 SQL 后,底层会操作原生 jdbc 的 API ,去数据库执行 SQL ,如果是查询的话,会返回查询结果集 ResultSet
;
MyBatis 拿到 ResultSet
后,由 ResultHandler
负责封装结果集,根据我们事先定义好的结果集类型,封装好结果集后返回。
这里面除了 SqlSessionFactory
和 SqlSession
之外,有两个相当重要的类需要我们前置了解,之前的章节我们只是遇到它们时简单的提一下,但现在我们要掌握全局的生命周期,就必须对它们也深入了解。下面我们来详细了解一下它们。
2. Executor
Executor
,字面意为 “执行器” ,它在整个 MyBatis 的执行流程中起到了 “枢纽” 的角色,SqlSession
的所有 SQL 执行,底层都是委托 Executor
执行,而 Executor
又是直接跟底层 jdbc 的 API 打交道的,所以我们一定要搞明白这个 Executor
的设计。
2.1 结构设计
Executor
本身是一个接口,它里面定义了好多方法,我们挑几个比较重要的方法先来看一下:
复制 public interface Executor {
int update ( MappedStatement ms , Object parameter) throws SQLException ;
< E > List < E > query ( MappedStatement ms , Object parameter , RowBounds rowBounds ,
ResultHandler resultHandler , CacheKey cacheKey , BoundSql boundSql) throws SQLException ;
< E > List < E > query ( MappedStatement ms , Object parameter , RowBounds rowBounds ,
ResultHandler resultHandler) throws SQLException ;
Transaction getTransaction ();
void commit ( boolean required) throws SQLException ;
void rollback ( boolean required) throws SQLException ;
// ......
}
可以发现,Executor
中真的把需要与数据库交互的操作,基本都定义好了,这里面大体包含这么多吧:
不过这也仅仅是定义的接口方法,下面肯定还有落地实现,Executor
本身有两个直接的抽象实现类,共 5 个最终落地实现类:
下面我们挑其中重要的实现类来讲解。
2.2 基础子类:BaseExecutor
Base 开头,很明显它就是个抽象的父类,不过这个 BaseExecutor
实现的倒是不少,大多都是逻辑骨架之类的代码实现,我们可以展开聊聊。
2.2.1 重要成员
这个类的成员有不少重要的,而且不乏一些我们之前看到过的:
复制 public abstract class BaseExecutor implements Executor {
private static final Log log = LogFactory . getLog ( BaseExecutor . class );
protected Transaction transaction;
protected Executor wrapper;
protected ConcurrentLinkedQueue < DeferredLoad > deferredLoads;
protected PerpetualCache localCache;
protected PerpetualCache localOutputParameterCache;
protected Configuration configuration;
protected int queryStack;
private boolean closed;
注释我们都标注好了,其中有几个重要的或者容易引起好奇心的,小册可以多说两嘴:
Executor wrapper
:Executor
本身也有装饰者的设计(比方说普通的 Executor
可以通过套一层装饰者,实现二级缓存的预检查和存储);
PerpetualCache localOutputParameterCache
:这个东西跟底层操作 jdbc 中 Statement 的类型有关,一般情况下我们不会接触到它,所以不用管了;
queryStack
:当遇到嵌套查询时,这个计数器可以及时的记录,并在合适的位置控制检查是否需要清空缓存。
简单了解一下就可以哈,下面遇到它们的时候,小册还会说的。
下面我们来看看 BaseExecutor
都提供了哪些基础的方法。
2.2.2 query
论基础且最重要的方法,那必属于我 CRUD 四大金刚了。我们先看查询的 query
方法吧:
复制 public < E > List< E > query( MappedStatement ms , Object parameter , RowBounds rowBounds ,
ResultHandler resultHandler) throws SQLException {
BoundSql boundSql = ms . getBoundSql (parameter);
CacheKey key = createCacheKey(ms , parameter , rowBounds , boundSql) ;
return query(ms , parameter , rowBounds , resultHandler , key , boundSql) ;
}
乍一看,这个 query
方法很简单啊,不要着急,这个方法只是个中转站,这里它主要的工作,是获取到要发送的 SQL ,以及根据要去往数据库的查询请求,构造出缓存的标识(即 CacheKey
),之后继续往下传。
下面重载的 query
方法才是重头戏,小册已经把整体流程都标注了注释,各位先整体扫一遍流程,理一下脉络:
复制 public < E > List< E > query( MappedStatement ms , Object parameter , RowBounds rowBounds ,
ResultHandler resultHandler , CacheKey key , BoundSql boundSql) throws SQLException {
ErrorContext . instance () . resource ( ms . getResource ()) . activity ( "executing a query" ) . object ( ms . getId ());
// 查询前的检查
if (closed) {
throw new ExecutorException( "Executor was closed." ) ;
}
// 如果是刚开始查询,并且statement定义的需要刷新缓存,则前置清空一次一级缓存
if (queryStack == 0 && ms . isFlushCacheRequired ()) {
clearLocalCache() ;
}
List < E > list;
try {
// 开始查询,计数器+1
queryStack ++ ;
// 先检查一级缓存中是否有查询结果
list = resultHandler == null ? ( List< E > ) localCache . getObject (key) : null ;
if (list != null ) {
// 如果有,直接返回
handleLocallyCachedOutputParameters(ms , key , parameter , boundSql) ;
} else {
// 否则,查询数据库
list = queryFromDatabase(ms , parameter , rowBounds , resultHandler , key , boundSql) ;
}
} finally {
// 查询完成后计数器-1
queryStack -- ;
}
// 计数器归零时,证明所有查询都已经完成,处理后续动作
if (queryStack == 0 ) {
// 处理延迟加载
for ( DeferredLoad deferredLoad : deferredLoads) {
deferredLoad . load ();
}
deferredLoads . clear ();
//如果全局配置文件中声明的一级缓存作用域是statement,则应该清空一级缓存(因为此时statement已经处理完毕了)
if ( configuration . getLocalCacheScope () == LocalCacheScope . STATEMENT ) {
clearLocalCache() ;
}
}
return list;
}
扫一遍下来,我们可以很清楚的从这段源码中获得这样的思路:
OK ,有这个思路就够了,具体的方法我们没有必要现在就着急深入探索,后面我们跑全流程的时候自然会深入的。
2.2.3 update
除了 query 之外,另一个重要的 CRUD 方法就是 update
了,我们都知道,insert update delete 语句都可以使用 update
的动作完成,所以 Executor
也只设计了一个 update
方法完事,BaseExecutor
中的 update
方法倒是简单:
复制 public int update( MappedStatement ms , Object parameter) throws SQLException {
ErrorContext . instance () . resource ( ms . getResource ()) . activity ( "executing an update" ) . object ( ms . getId ());
if (closed) {
throw new ExecutorException( "Executor was closed." ) ;
}
clearLocalCache() ;
return doUpdate(ms , parameter) ;
}
这很明显是模板方法的套路啊,doUpdate 才是真正干活的吧!我们往下翻一下:
复制 protected abstract int doUpdate( MappedStatement ms , Object parameter) throws SQLException;
嗯,果然不出我们所料 ~ 具体 doUpdate
都怎么实现,我们现在也是不着急深入哈,后面全流程走的时候自然会看到的。
哦对了,我们多留意一点,发现 doUpdate
方法是个模板方法的时候,下面居然还有个 doQuery
方法:
复制 protected abstract < E > List< E > doQuery( MappedStatement ms , Object parameter ,
RowBounds rowBounds , ResultHandler resultHandler , BoundSql boundSql) throws SQLException;
诶?这就奇了怪了,上面看 query
方法的时候,也没看到有 doQuery
方法啊,咋回事呢?如果小伙伴有这个疑问,我只能说一句你太棒了,你的主动思考能力就是驱使你去探索学习的源动力,请保持住这种感觉,加油 ~
至于 doQuery
方法搁哪儿调用的,我们同样也是跟 doUpdate
一块儿,后面再找。
2.2.4 其他方法
除了读和写的动作之外,Executor
中也负责事务的提交和回滚:
复制 public void commit( boolean required) throws SQLException {
if (closed) {
throw new ExecutorException( "Cannot commit, transaction is already closed" ) ;
}
clearLocalCache() ;
flushStatements() ;
if (required) {
transaction . commit ();
}
}
public void rollback( boolean required) throws SQLException {
if ( ! closed) {
try {
clearLocalCache() ;
flushStatements( true ) ;
} finally {
if (required) {
transaction . rollback ();
}
}
}
}
注意 Executor
不负责开启事务,因为事务在 Executor
的创建时期就已经传入进去了:
复制 protected BaseExecutor( Configuration configuration , Transaction transaction) {
this . transaction = transaction;
// ......
}
还有一些别的方法,我们后面走全流程的时候遇到再说吧,这里我们可以做到对 BaseExecutor
有一个整体的认识就可以了。
2.3 基本实现类:SimpleExecutor
下面我们来介绍 BaseExecutor
的一个重要的实现子类,它也是最简单最基础的实现类: SimpleExecutor
,由于 BaseExecutor
中有设计一些模板方法,SimpleExecutor
就只是负责把这些方法实现了,没有别的多余的设计。
首先是 doQuery
方法,这基本就是直接操作 jdbc 的 API 了:
复制 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 ();
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler , ms . getStatementLog()) ;
return handler . query (stmt , resultHandler);
} finally {
closeStatement(stmt) ;
}
}
接下来是 doUpdate
方法,发现套路几乎是一样的,只是一个调用 StatementHandler
的 query
方法,一个是调用 update
方法:
复制 public int doUpdate( MappedStatement ms , Object parameter) throws SQLException {
Statement stmt = null ;
try {
Configuration configuration = ms . getConfiguration ();
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler , ms . getStatementLog()) ;
return handler . update (stmt);
} finally {
closeStatement(stmt) ;
}
}
注意留意!这里面涉及到了之前在讲解 MyBatis 插件时说的 StatementHandler
!之前我们提到过,MyBatis 的插件 / 拦截器,可以拦截 StatementHandler
的部分方法,其中就有 query
和 update
方法,这里我们要稍微留意一下哈。
2.4 带二级缓存的实现类:CachingExecutor
再然后,是可以体现出 MyBatis 二级缓存的 CachingExecutor
了,我们之前在二级缓存的章节中遇见过它,不过当时小册只是顺口提了一下这个类名(毕竟当时我们要关注的是二级缓存的生效原理),现在我们也简单看看它。
请注意,CachingExecutor
本身是一个装饰者,所以它并没有继承自 BaseExecutor
(如果继承了,那就意味着 CachingExecutor
内部也会组合一个一级缓存,这显然不符合设计),而是只实现了 Executor
接口。装饰者,那内部一定要有一个 delegate
:
复制 public class CachingExecutor implements Executor {
private final Executor delegate;
private final TransactionalCacheManager tcm = new TransactionalCacheManager() ;
另外还有一个 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 了:
复制 public final class MappedStatement {
// 当前mapper的来源(mapper.xml / Mapper.class的路径)
private String resource;
private Configuration configuration;
private String id;
// statement内部封装的SQL
private SqlSource sqlSource;
// 当前statement对应的mapper.xml或Mapper接口的namespace下的二级缓存
private Cache cache;
// 如果是select,则此处存放返回值的映射(resultMap和resultType都在这里)
private List < ResultMap > resultMaps;
// 执行此条SQL之前是否需要清空二级缓存
private boolean flushCacheRequired;
// 当前SQL是否使用二级缓存
private boolean useCache;
// ......
}
可以发现,MappedStatement
中保存的其实就是 mapper.xml
或者注解 Mapper
接口中定义的那些元素的信息(有点元信息的味了),当然也有最最重要的 SQL 语句封装,注意它不是用一个普通的 String 去封装 SQL ,而是一个专门的 SqlSource
,为什么要设计它,我们马上就说。
3.2 SqlSource的设计
先解释一下为什么不用普通的 String 去封装 SQL 。
3.2.1 SqlSource的意义
其实原因很简单,比方说下面的一条 mapper 定义:
复制 < select id = "findAll" parameterType = "Department" resultType = "Department" >
select * from tbl_department
< where >
< if test = "id != null" >
and id = #{id}
</ if >
< if test = "name != null" >
and name like concat('%', #{name}, '%')
</ if >
</ where >
</ select >
请问这条 SQL 要如何封装呢?动态 SQL 在实际查询的时候,应该是根据传入的参数,动态的组合 if 标签来生成 SQL ,所以用 String 来记录 SQL 是不现实的。
3.2.2 SqlSource的定义
SqlSource
本身是一个接口,它只有一个方法,就是获取 SQL :
复制 public interface SqlSource {
BoundSql getBoundSql ( Object parameterObject);
}
注意这里它的返回值又不是 String ,而是一个 BoundSql
,看到这里可能小伙伴头顶上的问号越来越大了,这都图个啥啊???不要方,小册给你一个简单的图理解一下:
如图中所示,SqlSource
我们可以理解为带动态 SQL 标签的 SQL 定义 ,在程序运行期间,给 SqlSource
传入 SQL 必需的参数后,它会解析这些动态 SQL ,并生成一条真正可用于 PreparedStatement
的 SQL ,并且把这些参数也都保存好,而保存 SQL 和参数的载体就是 BoundSql
了。
当然,话又说回来,并不是所有的 SQL 定义都是动态的,也存在一些很简单的 SQL 呀(比方说 findById
这样的),这种情况下就没有必要用复杂 SQL 的模型组合了,所以 SqlSource
本身是有好几种实现的,下面我们来看它的一些实现。
3.2.3 简单实现:StaticSqlSource
静态的 SQL ,这个就是上面提到的,类似于 findAll
、findById
的那种 SQL ,它们的 SQL 定义都没有动态标签,MyBatis 底层就会用这种方式封装。
StaticSqlSource
的设计相当简单,底层就是封装的明文 SQL :
复制 public class StaticSqlSource implements SqlSource {
private final String sql;
private final List < ParameterMapping > parameterMappings;
private final Configuration configuration;
@ Override
public BoundSql getBoundSql ( Object parameterObject) {
return new BoundSql(configuration , sql , parameterMappings , parameterObject) ;
}
}
3.2.4 动态SQL:DynamicSqlSource
动态的 SQL ,对应的就是那些用了动态 SQL 标签的 statement 了,它的设计就不像 StaticSqlSource
那么简单了,它的底层是一个 SqlNode
:
复制 public class DynamicSqlSource implements SqlSource {
private final Configuration configuration;
private final SqlNode rootSqlNode;
@ Override
public BoundSql getBoundSql ( Object parameterObject) {
DynamicContext context = new DynamicContext(configuration , parameterObject) ;
rootSqlNode . apply (context);
SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration) ;
Class < ? > parameterType = parameterObject == null ? Object . class : parameterObject . getClass ();
SqlSource sqlSource = sqlSourceParser . parse ( context . getSql () , parameterType , context . getBindings ());
BoundSql boundSql = sqlSource . getBoundSql (parameterObject);
context . getBindings () . forEach (boundSql :: setAdditionalParameter);
return boundSql;
}
}
而且看下面 getBoundSql
方法的逻辑也比较奇怪,它又借助 DynamicContext
和 SqlSourceBuilder
这两个东西来辅助生成 SQL ,这几个东西又是干什么的呢?这里我们先不展开了,后面我们会讲解这里面的每一步动作。
3.2.5 基于Provider:ProviderSqlSource
还记得第 12 章我们有讲 Provider 系列的注解吗,使用 Provider 的方式定义的 statement ,MyBatis 会选用这种 SqlSource
封装。它的底层是一系列反射的元素:
复制 public class ProviderSqlSource implements SqlSource {
private final Configuration configuration;
// Provider对应的类
private final Class < ? > providerType;
private final LanguageDriver languageDriver;
private final Method mapperMethod;
// 提供SQL的方法
private final Method providerMethod;
private final String [] providerMethodArgumentNames;
private final Class < ? >[] providerMethodParameterTypes;
// 参数支持ProviderContext
private final ProviderContext providerContext;
private final Integer providerContextIndex;
这种方式我们平时用的不多,MyBatis 本身也不是很喜欢我们使用这种方式,所以小册也不展开研究了,感兴趣的小伙伴可以自行研究一下。
3.3 MappedStatement的重要方法
好了话说回来,MappedStatement
的内部核心有 SQL 的定义,也有这些 statement 对应的一些配置元信息的存储,除此之外,它还有一个重要的方法,就是直接从 MappedStatement
上解析 SQL :
复制 public BoundSql getBoundSql( Object parameterObject) {
BoundSql boundSql = sqlSource . getBoundSql (parameterObject);
List < ParameterMapping > parameterMappings = boundSql . getParameterMappings ();
if (parameterMappings == null || parameterMappings . isEmpty ()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
// check for nested result maps in parameter mappings (issue #30)
for ( ParameterMapping pm : boundSql . getParameterMappings ()) {
String rmId = pm . getResultMapId ();
if (rmId != null ) {
ResultMap rm = configuration . getResultMap (rmId);
if (rm != null ) {
hasNestedResultMaps |= rm . hasNestedResultMaps ();
}
}
}
return boundSql;
}
说是重要,其实也没有多重要,因为它内部就是转发了一层而已,实际干活的还是上面提到的 SqlSource
。