Mybatis(五) - SqlSession执行流程

前面两章,我们终于走完了 MyBatis 初始化阶段,接下来的几章,主要的重心会放在执行阶段。

本章我们来看一看,SqlSession 的构造,以及在执行 statement 时,它底层都干了什么。

1. SqlSession的创建

SqlSession 的创建,要来源于 SqlSessionFactoryopenSession 方法,而这个 openSession 方法重载的实在是有点多:

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

小伙伴们别被吓到,虽说是那么多,下面的几种我们用到过吗?肯定没有吧,所以我们也不用关心,只需要看默认的无参方法即可。

1.1 openSession

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx); // may have fetched a connection so lets call close()
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    } finally {
        ErrorContext.instance().reset();
    }
}

这段源码我们眼熟,在第 17 章讲解事务管理器的时候还特意看过,事务工厂就是在这里创建的,不过这里我们要关心的不是事务工厂了,而是 SqlSession 的创建。

大概看一下创建 SqlSession 都需要哪些因素:

  • EnvironmentTransactionFactory 事务工厂。

  • Executor 真正的执行器。

  • Configuration 全局配置。

还记得吧,SqlSession 本身不是真正负责执行 CRUD 操作的,而是会转交给 Executor 来干,这里我们可以看一下,Configuration 是如何创建 Executor 的。

1.1.1 configuration.newExecutor

来到源码中,这段源码本身并不复杂:

可以发现,它就是根据指定的 executorType ,决定创建哪种类型的 Executor ,并在必要的时候包装一下二级缓存的增强。这几个 Executor 的实现类,我们在第 22 章中已经见过了,不过当时我们只讲解了 SimpleExecutorCachingExecutor ,是因为这两个实现类是我们最常遇到的,如果小伙伴们有忘记这两个实现类,可以返回去再看看。

另外留意一下最底下,MyBatis 的插件会给 Executor 增强,这个增强逻辑本身也不复杂:

可以发现,就是普通的 jdk 动态代理而已,也没什么好说的,相信各位一看都明白。

1.1.2 DefaultSqlSession的设计

上面的组件都准备就绪后,最终会创建出一个 DefaultSqlSession 的对象,这个 DefaultSqlSession 本身是 SqlSession 的最基础实现类,它的内部结构如下:

除了我们上面看到的 ConfigurationExecutor 之外,还有一个比较奇怪的属性:dirty ,它是干什么的呢?

1.1.2.1 dirty的设计

这个属性其实从字面意思上理解,它是标记当前这个 SqlSession 是不是脏的,这个 “脏” 如何去理解呢?我们可以看看这个 dirty 属性都在哪些地方被设置:

img

可见只有一处设置了 dirty 为 true ,而这个位置,对应的是 SqlSessionupdate 方法:

这里小册先提一句,对于 insertupdatedelete 这样会对数据造成影响的操作,最终都是走 update 方法执行 SQL 语句。那既然是这些造成了数据影响的操作执行了,对于整个 SqlSession 来讲,如果事务开启时,那它现在所能查到的数据,就与真实数据库中不一致了,即 “脏” 了。

那如何让它变回干净的状态呢?很简单,提交 / 回滚事务,都可以让 SqlSession 重回干净状态(此时 SqlSession 查询到的数据与数据库一致)。

好了回到主线上,SqlSession 的创建本身还是不太复杂的,我们还是继续往下看。

2. 执行SqlSession的方法

SqlSession 本身可以供我们执行的方法还是很多的,下面我们分门别类的看。

2.1 select系列

SqlSession 中定义的 select 系列的方法是最多的,借助 IDE 我们可以看到方法的类型分为以下几种:

img

下面小册就这几类方法逐一解释。

2.1.1 selectList

selectList 方法是我们最常用的方法之一,它可以直接返回一个数据的列表,泛型也是由我们自己指定。这个方法是 SqlSession 系列最通用的方法,我们可以来看看它的实现:

可以发现它相当于是做了一次转发,先从全局 Configuration 中取出对应的 MappedStatement ,随后就交给 Executor 去真正执行了。

好,那我们就准备继续往里走,不过在此之前我们先留意下这里面的一个方法:wrapCollection

2.1.1.1 wrapCollection

这个 wrapCollection 方法,从字面上看,它是要把参数包装为集合?还是要包装集合呢?不知道,咱还是点击去看看吧:

哦,原来它是将我们调用 statement 传参时,兼容集合和数组类型的参数做的工作。

一般情况下我们调用 SqlSession 的方法时,都是传入一个模型对象,或者 Map 集合,不过也有一些情况需要传入数组 / 列表( findByIdsdeleteByIds ),这种情况下在 mapper.xml 中我们怎么取这个数组 / 集合呢?答案就在这里,对于数组,它会帮我们起一个默认的名 array ,而对于列表,我们可以使用 collection 或者 list 获取到。

2.1.1.2 executor执行

包装好参数后,下面就会进入到 Executor 中了,这里面的逻辑更为重要,小伙伴们打起精神来呀。

Executorquery 方法又被分为 3 个步骤,分别是构造 SQL 、构造缓存键、真正的查询动作。

  1. 构造 SQL ,这个步骤会涉及到动态 SQL 的参数绑定动作,由于该部分略复杂,小册打算放到下一章来讲解;

  2. 构造缓存键,这个东西与一级缓存有关,下面我们马上就会看到;

  3. query 真正的查询。

针对后两个方法,我们继续往下拆解。

CacheKey的构造

缓存键的设计,其实是为了分辨出每次 SqlSession 的查询时,都是用的哪个 statement ,用了什么 SQL ,传了什么参数。缓存键会把这些要素都保存起来,封装为一个 CacheKey 对象。

源码的实现不很复杂,小册把注释标注好,小伙伴们捋一遍就可以了,没有必要深入探究。

下面是一个测试的 Debug 过程中,截取的 CacheKey 的结构,可以发现还是比较简单的:

img

2.1.1.3 query

继续往下走 Executorquery 方法,这个方法一进来,小伙伴是不是有一种似曾相识的感觉?对了,这就是一级缓存中我们看过的源码:

所以具体里面都是怎么走的,一级缓存如何处理的,小册也不用再重复唠叨了吧,想必各位如果看过第 14 章的原理解析部分,应该很熟悉了。

刨去外头的部分,至于底层如何真正的查询数据库,那就是中间的 queryFromDatabase 方法要干的了,我们继续往里走。

2.1.1.4 queryFromDatabase

来到 queryFromDatabase 方法中,我们看着还是不陌生,因为这里面有一级缓存的放置动作:

而真正从数据库中查询出来的那个 list 集合,则是走的 doQuery 方法:(又是 querydoQuery

呦,到了这里变成抽象方法了,那我们就应该找相应的实现呀。

2.1.1.5 SimpleExecutor#doQuery

上面的 SqlSession 创建中,我们知道一般情况下我们用的是 SimpleExecutor ,所以我们来到它的 doQuery 方法中:

注意看注释,这里面的 API 容易让小伙伴迷糊,所以小册会慢一点解释。

  1. 一开始在 try-catch 块上面定义的那个 Statement 类型的变量,是 jdbc 原生的 Statement

  2. 下面 try-catch 块中,它会使用全局 Configuration 对象去创建一个 StatementHandler

这个家伙我们在插件的那一章见过它,这里我们暂且不展开聊这个 StatementHandler ,下一章讲解参数绑定时再聊

  1. 再往下,它会利用 StatementHandler ,创建出真实的 Statement 对象,而这个创建的过程,就会用到 StatementHandler 的方法了:

  1. 最后,再调用 StatementHandlerquery 方法,真正发起查询:(一般情况下我们用的都是 PreparedStatement ,所以相应的 Handler 也就是 PreparedStatementHandler

注意,这里又出现了另一个 Handler :ResultSetHandler ,它是用来处理结果集和封装的,这个我们放到第 27 章讲解。

经过最后的 ResultSetHandler 处理和封装结果集,数据也就从数据库中查出来了,也就回到上面的 queryFormDatabase 了,下面的步骤就是放一级缓存,返回数据给 SqlSession ,进而返回给调用 API 的客户端代码了。

整个流程走完之后,小册贴一张 selectList 的整体调用时序图,方便小伙伴们快速总结。

img

2.1.2 selectOne

对于 selectOne 的处理,MyBatis 可谓是深得其精髓:先查出列表来,然后取第 0 个就完事了。对应到源码中,它的设计如下:

可以看到,这里 MyBatis 还多做了一点处理:如果查出来的数据超过 1 条,不会直接返回第 0 条数据,而是抛出异常。这个设计小伙伴们一定要注意,因为有些小伙伴会认为,如果真的查出来数据超过 1 条的话,那你干脆返回第 0 条数据得了,为啥还要抛个异常影响我正常用呢?诶,这就是 MyBatis 本身的设计了,如果小伙伴不喜欢这种设计,那就直接用 selectList 吧,先把列表查出来,然后用一个三元运算符处理一下就 OK ( return list.size() > 0 ? list.get(0) ? null )。

2.1.3 selectMap

下面的几个方法是我们平时用得不多的,不过我们也可以看一下。

selectMap 方法相当于增强版 selectList ,它可以为每一条查询出来的结果提供一个可以检索的 key ,比方说这样:

img

上图中,我们把查询结果的 id 列作为 Map 的 key ,查询的一行结果数据作为 value ,就可以构造出一个基于 Map 的数据库查询结果集了。

下面我们看源码的实现,其实 selectMap 还是基于 selectList 来的,只是最后多了一些处理罢了:(关键注释已标注在源码)

我们可以来捋一下这段逻辑,它从数据库中查出数据后,会先创建两个对象:DefaultMapResultHandlerDefaultResultContext ,然后通过 DefaultMapResultHandlerhandleResult 方法一步一步的封装,最后取到封装好的 Map 返回。这里面我们最好奇的方法肯定是 DefaultMapResultHandler 的了,下面我们进去看一下。

2.1.3.1 DefaultMapResultHandler

DefaultMapResultHandler 本身的结构并不复杂,贴出构造方法,除了是让小伙伴们了解一下所需要的属性,还有一个细节需要各位关注一下:

注意源码中构造方法的倒数第二行,它创建 Map 的方法是借助 ObjectFactory ,直接创建 Map 接口的实现类,这个实现类在底层的落地是 **HashMap**

那这就意味着一个问题:**selectMap** 处理完成的 **Map** 数据,迭代时的顺序与 **selectList** 大概率不一致!如果我们想让封装后的 Map 依然保证迭代的顺序一致性,则需要自己编写 ObjectFactory 的实现类 / DefaultObjectFactory 的子类,并重写 resolveInterface 方法,替换 Map 接口的落地实现类为 LinkedHashMap 。至于怎么编写和配置,在第 6 章小册已经讲过了,忘记的小伙伴可以回过头去翻一下。

2.1.3.2 封装Map的过程

下面是封装 Map 的过程,其实很简单,就是从 DefaultResultContext 中取到正在迭代的数据,取出来,反射获取指定列(即 mapKey )对应的值,并存入 Map 中,逻辑很简单清晰。

2.1.4 selectCursor

Cursor 游标,是 MyBatis 3.4.0 新增的特性,它适合处理大数据集结果。Cursor 的设计本身类似于 ResultSet ,因为不是一次性查出放到内存,所以对内存消耗的影响也小。

DefaultSqlSession 中设计的 selectCursor 的逻辑也不复杂,它很类似于 selectList

注意看上面,从数据库中查出数据,并封装为游标后,会有一个注册的动作,而这个注册的动作,在 DefaultSqlSession 中就是一个再简单不过的 list 添加:

这个时候,观察思维能力很强的小伙伴应该意识到一个问题:cursorList 不可能只有添加,肯定还有清除,那什么时候会清除呢?答案是在 SqlSession 关闭的时候:

SqlSession 关闭时,会将自身查询出来的游标也一并关闭掉,这就意味着 SqlSession 关闭后,我们就不能再利用那些游标,从数据库中取出数据了。

2.1.5 select()

如果上面的几种方法,都不能满足我们需求的时候,我们还可以直接用 MyBatis 提供的自定义结果集封装的方法,自行处理,这个方法就是没有任何后缀的 select 方法:

注意看参数,它需要让我们自己传一个 ResultHandler 接口的实现类,用于封装结果集。这个 ResultHandler 会在Executorquery 方法执行,底层封装结果集时起作用,而封装结果集的动作,小册放到第 27 章讲解。

以上就是 SqlSession 提供的执行 DQL 的方法,内容比较多,小伙伴们注意区分。

2.2 update系列

MyBatis 认为 insert update delete 的操作,底层都是执行 DML ,所以它偷懒只在 update 方法上有实际的实现,其余的方法都是调用了它。

下面我们来看 update 方法的实现。

2.2.1 update的实现

执行 update 方法时,首先标注当前 SqlSession 已经不干净了(有过 DML 操作),随后又是取出 MappedStatement ,并调用 Executorupdate 方法。

2.2.2 update → doUpdate

Executorupdate 方法,又会调用模板方法 doUpdate

doUpdate 方法的落地实现,我们依然关注 SimpleExecutor ,但是我们点进来,发现好像有点似曾相识:

跟上面的 doQuery 对比一下,好像就最后一步,调用 StatementHandler 的方法不一样吧!说明真正调用原生 jdbc 的操作,都在 StatementHandler 中。

2.2.3 StatementHandler#update

进入到 PreparedStatementHandlerupdate 方法,可以发现它就是操作原生 jdbc ,执行 PreparedStatementexecute 方法,并获取 DML 的执行影响结果行数,返回。

注意一点,在获取到返回行数后,MyBatis 又操作 KeyGenerator 执行了一个后置处理,它是用来干什么的呢?

回想一下,如果数据库表的主键采用自增主键,那是不是每次 insert 后我们都需要获取到主键值,以保证可以正常返回给客户端代码。而这个 KeyGenerator 的工作,就是帮我们处理自增主键并回填的。内部的原理不算很复杂,感兴趣的小伙伴可以自行深入看一下,小册不再展开。

OK ,到这里,SqlSessionExecutor 级别的操作,我们就算走完了,内容比较多,小伙伴们注意记录、对比和总结。

最后更新于

这有帮助吗?