MyBatis源码学习(三)(下)

简介: 前言文本已收录至我的GitHub仓库,欢迎Star:github.com/bin39232820…种一棵树最好的时间是十年前,其次是现在


查询语句的执⾏过程


查询语句对应的方法比较多,有如下几种:

  • executeWithResultHandler
  • executeForMany
  • executeForMap
  • executeForCursor

这些方法在内部调用了 SqlSession 中的一些 select方法,比如 selectList、selectMap、 selectCursor 等。这些方法的返回值类型是不同的,因此对于每种返回类型,需要有专门的处 理方法。以 selectList 方法为例,该方法的返回值类型为 List。但如果我们的 Mapper 或 Dao 的接口方法返回值类型为数组,或者 Set,直接将 List 类型的结果返回给 Mapper/Dao 就不合 适了。execute等方法只是对 select等方法做了一层简单的封装,因此接下来我们应们应该 把目光放在这些 select方法上。


selectOne ⽅法分析

本节选择分析 selectOne 方法,而不是其他的方法,大家或许会觉得奇怪。前面提及了 selectList、selectMap、selectCursor 等方法,这里却分析一个未提及的方法。这样做并没什么 特别之处,主要原因是 selectOne 在内部会调用 selectList 方法。这里分析 selectOne 方法是 为了告知大家,selectOne 和 selectList 方法是有联系的,同时分析 selectOne 方法等同于分析 selectList 方法。如果你不信的话,那我们看源码吧,源码面前了无秘密。

// -☆- DefaultSqlSession
public <T> T selectOne(String statement, Object parameter) {
// 调用 selectList 获取结果
List<T> list = this.<T>selectList(statement, parameter);
if (list.size() == 1) {
// 返回结果
return list.get(0);
 } else if (list.size() > 1) {
// 如果查询结果大于 1 则抛出异常,这个异常也是很常见的
throw new TooManyResultsException("……");
 } else {
return null;
 } }
复制代码



这个异常我不信大家没有碰到过,哈哈,只有读过源码才知道怎么去解决这些异常,怎么说呢 以前小六六碰到一个api不会用 我第一时间就是百度,碰到异常 也是,但是现在 对于我们经常用的框架 我会点进去看源码了,感觉慢慢的有点进步了,当然不熟悉的框框我还是百度,哈哈。

如上,selectOne 方法在内部调用 selectList 了方法,并取 selectList 返回值的第 1 个元素 作为自己的返回值。如果 selectList 返回的列表元素大于 1,则抛出异常。上面代码比较易懂, 就不多说了。下面我们来看看 selectList 方法的实现。

// -☆- DefaultSqlSession
public <E> List<E> selectList(String statement, Object parameter) {
// 调用重载方法
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
复制代码


private final Executor executor;
public <E> List<E> selectList(String statement, Object parameter, RowBounds
rowBounds) {
try {
// 获取 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 调用 Executor 实现类中的 query 方法
return executor.query(ms, wrapCollection(parameter), 
rowBounds, Executor.NO_RESULT_HANDLER);
 } catch (Exception e) {
throw ExceptionFactory.wrapException("……");
 } finally {
ErrorContext.instance().reset();
 } }
复制代码


如上,这里要来说说 executor 变量,该变量类型为 Executor。Executor 是一个接口,它 的实现类如下:


Executor 有这么多的实现类,大家猜一下 executor 变量对应哪个实现类。要弄清楚这个 问题,需要大家到源头去查证。这里提示一下,大家可以跟踪一下 DefaultSqlSessionFactory 的 openSession 方法,很快就能发现 executor 变量创建的踪迹。限于篇幅原因,本文就不分 析 openSession 方法的源码了。默认情况下,executor 的类型为 CachingExecutor,该类是一 个装饰器类,用于给目标 Executor 增加二级缓存功能。那目标 Executor 是谁呢?默认情况 下是 SimpleExecutor



现在大家搞清楚 executor 变量的身份了,接下来继续分析 selectOne 方法的调用栈。先 来看看 CachingExecutor 的 query 方法是怎样实现的。如下:

// -☆- CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, 
RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 获取 BoundSql
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 创建 CacheKey
CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
// 调用重载方法
return query(ms, parameterObject, 
rowBounds, resultHandler, key, boundSql);
}
复制代码


上面的代码用于获取 BoundSql 对象,创建 CacheKey 对象,然后再将这两个对象传给重 载方法。BoundSql 的获取过程较为复杂,我将在下一节进行分析。CacheKey 以及接下来即 将出现的一二级缓存将会独立成章分析。 上面的方法等代码和 SimpleExecutor 父类 BaseExecutor 中的实现没什么区别,有区别的 地方在于这个方法所调用的重载方法。继续往下看

// -☆- CachingExecutor
public <E> List<E> query(MappedStatement ms, Object parameterObject, 
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, 
BoundSql boundSql) throws SQLException {
// 从 MappedStatement 中获取缓存
Cache cache = ms.getCache();
// 若映射文件中未配置缓存或参照缓存,此时 cache = null
if (cache != null) {
flushCacheIfRequired(ms);
if (ms.isUseCache() && resultHandler == null) {
ensureNoOutParams(ms, boundSql);
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list == null) {
// 若缓存未命中,则调用被装饰类的 query 方法
list = delegate.<E>query(ms, parameterObject, 
rowBounds, resultHandler, key, boundSql);
tcm.putObject(cache, key, list); // issue #578 and #116
 }
return list;
 } }
// 调用被装饰类的 query 方法
return delegate.<E>query(
ms, parameterObject, rowBounds, resultHandler, key, boundSql);
}
复制代码


以上代码涉及到了二级缓存,若二级缓存为空,或未命中,则调用被装饰类的 query 方 法。下面来看一下 BaseExecutor 的中签名相同的 query 方法是如何实现的。

// -☆- BaseExecutor
public <E> List<E> query(MappedStatement ms, Object parameter, 
RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, 
BoundSql boundSql) throws SQLException {
if (closed) {
throw new ExecutorException("Executor was closed.");
}
if (queryStack == 0 && ms.isFlushCacheRequired()) {
clearLocalCache();
 }
List<E> list;
try {
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 {
queryStack--; }
if (queryStack == 0) {
// 从一级缓存中延迟加载嵌套查询结果
for (DeferredLoad deferredLoad : deferredLoads) {
deferredLoad.load();
 }
deferredLoads.clear();
if (configuration.getLocalCacheScope()==LocalCacheScope.STATEMENT) {
clearLocalCache();
 }
 }
return list; }
复制代码


上面的方法主要用于从一级缓存中查找查询结果,若缓存未命中,再向数据库进行查询。 在上面的代码中,出现了一个新的类 DeferredLoad,这个类用于延迟加载。该类的实现并不 复杂,但是具体用途让我有点疑惑。这个我目前也未完全搞清楚,就不分析了。接下来,我 们来看一下 queryFromDatabase 方法的实现

// -☆- BaseExecutor
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 {
// 调用 doQuery 进行查询
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; }
复制代码


上面的代码仍然不是 selectOne 方法调用栈的终点,抛开缓存操作,queryFromDatabase 最终还会调用 doQuery 进行查询。所以下面我们继续进行跟踪。

// -☆- SimpleExecutor
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
StatementHandler handler = configuration.newStatementHandler(
wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 创建 Statement
stmt = prepareStatement(handler, ms.getStatementLog());
// 执行查询操作
return handler.<E>query(stmt, resultHandler);
 } finally {
// 关闭 Statement
closeStatement(stmt);
 } }
复制代码


doQuery 方法中仍然有不少的逻辑,完全看不到即将要到达终点的趋势,不过这离终点 又近了一步。接下来,我们先跳过 StatementHandler 和 Statement 创建过程,这两个对象的创 建过程会在后面进行说明。这里,我们以 PreparedStatementHandler 为例,看看它的 query 方 法是怎样实现的。如下:

// -☆- PreparedStatementHandler
public <E> List<E> query(Statement statement, ResultHandler resultHandler) 
throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 执行 SQL
ps.execute();
// 处理执行结果
return resultSetHandler.<E>handleResultSets(ps);
}
复制代码


到这里似乎看到了希望,整个调用过程总算要结束了。不过先别高兴的太早,SQL 执行 结果的处理过程也很复杂,稍后将会专门拿出一节内容进行分析。 以上就是 selectOne 方法的执行过程,尽管我已经简化了代码分析,但是整个过程看起来还是很复杂的。查询过程涉及到了很多方法调用,不把这些调用方法搞清楚,很难对 MyBatis 的查询过程有深入的理解。


结尾


其实我也只是跟着大佬的书在读,很多东西也是一知半解,哈哈 但是今天我们主要讲的是一个selectOne的执行过程,这边我们来总结一下,首先就是DefaultSqlSession调用selectOne,然后其实他这个方法调用的是selectList 然后通过list.size() > 1来看是否需要抛出异常,然后selectList里面其实是调用了executor.query方法,然后就得说一下Executor是怎么来的了,他是在调用openSession的时候通过config.newExecutor 生成的 默认情况下,executor 的类型为 CachingExecutor,那么接下来就是来看CachingExecutor.query 里面有几个步骤这边就不一一分析了(涉及一二级缓存),然后他接下来调用的是 query 然后就是调用BaseExecutor queryFromDatabase 这是去调用数据库的方法 然后是doQuery  往下就是类似于 JDBC的操作了以上就是一个查询的整个过程。

相关文章
|
4月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
13天前
|
缓存 NoSQL Java
Mybatis学习:Mybatis缓存配置
MyBatis缓存配置包括一级缓存(事务级)、二级缓存(应用级)和三级缓存(如Redis,跨JVM)。一级缓存自动启用,二级缓存需在`mybatis-config.xml`中开启并配置映射文件或注解。集成Redis缓存时,需添加依赖、配置Redis参数并在映射文件中指定缓存类型。适用于查询为主的场景,减少增删改操作,适合单表操作且表间关联较少的业务。
|
3月前
|
Java 关系型数据库 MySQL
springboot学习五:springboot整合Mybatis 连接 mysql数据库
这篇文章是关于如何使用Spring Boot整合MyBatis来连接MySQL数据库,并进行基本的增删改查操作的教程。
481 0
springboot学习五:springboot整合Mybatis 连接 mysql数据库
|
3月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
97 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
3月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
228 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
4月前
|
Java 关系型数据库 数据库连接
mybatis-plus学习
MyBatis-Plus ,MyBatis 最佳搭档,只做增强不做改变,为简化开发、提高效率而生。
59 5
|
5月前
|
XML Java 数据库连接
mybatis源码研究、搭建mybatis源码运行的环境
这篇文章详细介绍了如何搭建MyBatis源码运行的环境,包括创建Maven项目、导入源码、添加代码、Debug运行研究源码,并提供了解决常见问题的方法和链接到搭建好的环境。
mybatis源码研究、搭建mybatis源码运行的环境
|
5月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
5月前
|
供应链 前端开发 Java
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
该博客文章介绍了一个使用Mybatis、Layui、MVC和JSP技术栈开发的服装库存管理系统,包括注册登录、权限管理、用户和货号管理、库存管理等功能,并提供了源码下载链接。
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
|
5月前
|
Java 数据库连接 mybatis
后端框架的学习----mybatis框架(9、多对一处理和一对多处理)
这篇文章介绍了在MyBatis框架中如何处理多对一和一对多的关联查询,通过定义`<resultMap>`和使用`<association>`与`<collection>`元素来实现对象间的关联映射。