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
之外,有两个相当重要的类需要我们前置了解,之前的章节我们只是遇到它们时简单的提一下,但现在我们要掌握全局的生命周期,就必须对它们也深入了解。下面我们来详细了解一下它们。
Executor
,字面意为 “执行器” ,它在整个 MyBatis 的执行流程中起到了 “枢纽” 的角色,SqlSession
的所有 SQL 执行,底层都是委托 Executor
执行,而 Executor
又是直接跟底层 jdbc 的 API 打交道的,所以我们一定要搞明白这个 Executor
的设计。
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
中真的把需要与数据库交互的操作,基本都定义好了,这里面大体包含这么多吧:
CRUD 操作。
事务控制和获取。
二级缓存的控制。
延迟加载等。
不过这也仅仅是定义的接口方法,下面肯定还有落地实现,Executor
本身有两个直接的抽象实现类,共 5 个最终落地实现类:
下面我们挑其中重要的实现类来讲解。
Base 开头,很明显它就是个抽象的父类,不过这个 BaseExecutor
实现的倒是不少,大多都是逻辑骨架之类的代码实现,我们可以展开聊聊。
这个类的成员有不少重要的,而且不乏一些我们之前看到过的:
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
都提供了哪些基础的方法。
论基础且最重要的方法,那必属于我 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 ,有这个思路就够了,具体的方法我们没有必要现在就着急深入探索,后面我们跑全流程的时候自然会深入的。
除了 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
一块儿,后面再找。
除了读和写的动作之外,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
有一个整体的认识就可以了。
下面我们来介绍 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
方法,这里我们要稍微留意一下哈。
再然后,是可以体现出 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**
。
跟 Executor
同等重要的,还有它下游的 MappedStatement
,这个 Statement
的概念,就是我们从一开始学习 MyBatis 的时候提到的,mapper.xml 中写的一个一个的 <select>
也好,<insert>
等等也好,这些都会在底层封装为一个一个的 MappedStatement
,前面在解析 mapper.xml 和注解 Mapper 接口时我们也都看到了基本的脉络,本章我们重点关注它的结构。
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
,为什么要设计它,我们马上就说。
先解释一下为什么不用普通的 String 去封装 SQL 。
其实原因很简单,比方说下面的一条 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 是不现实的。
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
本身是有好几种实现的,下面我们来看它的一些实现。
静态的 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);
}
}
动态的 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 ,这几个东西又是干什么的呢?这里我们先不展开了,后面我们会讲解这里面的每一步动作。
还记得第 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 本身也不是很喜欢我们使用这种方式,所以小册也不展开研究了,感兴趣的小伙伴可以自行研究一下。
好了话说回来,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
。