Mybatis SQL执行过程
ztj100 2025-01-07 17:23 11 浏览 0 评论
开篇
mybatis版本:3.5.12
JDBC视角看数据库操作
我们知道,MyBatis是对JDBC的封装。让程序员从复杂繁琐的JDBC编程中解放双手。简单回顾一下JDBC编程的过程。通常分为这么几步:
- 加载驱动
- 获取连接Connection对象
- 从Connection中获取Statement对象(或者是PreparedStatement)对象
- 用户准备SQL语句
- 使用Statement对象执行SQL语句获得结果集对象——ResultSet
- 解析ResultSet对象,从ResultSet中获取需要的值。
- 关闭资源
以上7个步骤之后就统称传统JDBC了。传统JDBC的缺点太多了:其中1、2、3、5、6、7都是重复性的工作。JDBC中真正由用户确定的核心逻辑是SQL,理想中的对数据库的操作应该是简洁的。用户提供SQL并指定返回的结果集类型,程序执行SQL并自动返回解析后的结果集。
当然JDBC也有其他的缺点,综合来讲主要缺点为:
- 对事务的控制管理
- 连接资源的管理
- 代码复用性
- 其他高级设置(缓存等)
不过JDBC的缺陷就是天生的,Java只提供原生的操作,无论框架再怎么简单易用,底层的操作还是最基本的JDBC。
MyBatis解决了上述JDBC编程中的问题。对JDBC进行了封装。MyBatis框架的核心思想就是:用户提供SQL和返回值类型。其他的什么比如是否缓存、资源何时关闭、事务控制等全部交给框架来做。用户只需要做两件事——提供SQL和返回值类型。
MyBatis视角看数据库操作
在MyBatis中,既然用户解放了,那框架一定干的活多了。接下来就来探讨下MyBatis内部是如何封装那些重复性的操作的。从MyBatis的角度看,执行一条SQL需要经过这么几个步骤
- 读取数据库配置(账号、密码、文件资源位置等)
- 获取会话对象(SqlSession,相当于一个命令行黑窗口界面。可以操作SQL语句)
- 执行用户提供的SQL并返回结果集(包含Mapper方式)
- 自动释放资源
下面是代码描述
// 1. 读取数据库配置(账号、密码、文件资源位置等)
InputStream is = Resources.getResourceAsStream("sqlMapConfig.xml");
// 2. 获取会话对象(SqlSession,相当于一个命令行黑窗口界面。可以操作SQL语句)
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
// 3. 执行用户提供的SQL并返回结果集。(Mapper使用方式后面介绍)
List<Object> userList = sqlSession.selectList("org.apache.ibatis.amy.mapper.UserMapper.selectUser");
// 4. 自动释放资源(既然是自动的,用户就无需写代码了
复制代码
注:接下来,整个文章将对MyBatis的这4个步骤展开详细讨论。本系列文章本着由浅入深的理念将逐步揭开MyBatis的面纱。首先从应用入手,MyBatis是如何执行SQL的,如何获取返回的结果集,了解了执行SQL的大致过程后,再带着问题逐步深挖源码。接下来就是探究配置文件是如何被加载的,又是如何被使用的。最后介绍MyBatis的一些扩展点,插件机制、ObjectFactory等。
SQL如何执行
通过开篇,我们已经了解到MyBatis只需要用户提供SQL而无需其他操作就可以完成对数据库的查询。那么SQL到底是如何被MyBatis执行的呢?
大致流程
在看源码前先简单介绍一下几个重要对象以及他们之间的关系。
Class | 作用 |
SqlSession | SqlSession提供了CRUD的方法,通过调用select/update/insert/delete方法(参数是SQL存储位置),就可以完成对数据库的查询 |
Executor | 执行器;SqlSession中的内部属性。SqlSession会委托Executor来执行SQL语句。还包括一些复杂操作。比如缓存等,就是在Executor中完成的。 |
StatementHandler | SqlSession的方法参数并不是直接的SQL,而是SQL存储的位置。那么Executor执行的SQL语句就是由StatementHandler根据指定位置解析出来的。 |
ResultSetHandler | ResultSetHandler;用来处理结果集对象。比如Select user_name from user; 其中结果集中的user_name和实体User的userName属性关联,就是由ResultSetHandler完成的 |
TypeHandler | JDBC类型——Java类型的转换 |
ParameterHandler | PreparedStatement的参数设置 |
交接了大概执行步骤后,接下来带着这个思路看源码。
具体流程
我们借用开篇的示例来看一下
InputStream is = Resources.getResourceAsStream("sqlMapConfig.xml");
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(is);
SqlSession sqlSession = factory.openSession();
List<Object> userList = sqlSession.selectList("org.apache.ibatis.amy.mapper.UserMapper.selectUser");
// 后续用户自己的操作
复制代码
主要关注下最后一行代码。调用SqlSession对象提供的selectList方法,参数是SQL的位置(本例中指org.apache.ibatis.amy.mapper包下的UserMapper.xml文件中的selectUser标签)。
SqlSession#selectList方法内部会根据参数找到具体的SQL位置。
SqlSession#selectList方法
SqlSession是一个接口类,它有2个实现类。我们关注DefaultSqlSession即可。selectList有很多重载方法,最终都会调用到如下这个方法,接下来看下DefaultSqlSession#selectList的主要逻辑(省略非核心代码)
private <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) {
// 1. 通过配置对象获取MappedStatement对象,MappedStatement中包含了解析后的SQL语句。
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.query(ms, wrapCollection(parameter), rowBounds, handler);
}
复制代码
可以看到selectList方法只做了两件事
- 通过配置对象获取MappedStatement对象,MappedStatement中包含了解析后的SQL语句。(ms对象的具体实现现在不必关心,只需要知道它其中分装好了SQL即可)为了方便理解,不妨给他起个名字——后面我们就称它为sql包装对象
- executor是DefaultSqlSession中的一个属性。它通过调用query方法,根据sql包装对象(MappedStatement) 和用户提供的参数来查询数据库。
Executor#query方法
Executor也是一个接口对象。它提供了一系列的方法(query/update)方法完成对数据库的CRUD**(查询是query方法,增删改都是update方法)**
它有接口体系如下
看源码的过程中(注意是看源码的过程中哦)最常用的是SimpleExecutor和BaseExecutor。我们只需要关注这两个类的方法实现就好了。而query方法实在BaseExecutor中实现的。它也有很多重载的方法,但是最终都会调用到一个query方法。下面来看一下BaseExecutor#query方法的核心逻辑(非核心代码省略)
// RowBounds是内存分页对象。几乎没什么使用场景。忽略该对象即可
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. BoundSql存储的就是可执行的SQL和用户参数
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 创建缓存key,把它当成复杂的Map数据结构的的key值就行了。
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 3. 最终都会调用到这个query的重载方法中
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 1. 从一级缓存中获取对象(第一次肯定是没有的)
List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// (存储过程相关)
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 2. 没查到缓存就查数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
return list;
}
复制代码
我们可以看到Executor#query方法的大致执行逻辑是
- 从sql包装对象(MappedStatement) 中获取真实的SQL
- 根据sql和一系列参数创建缓存的key值。该key值能在一级缓存中唯一确定一个对象。
- 根据CacheKey(缓存key)从一级缓存中获取查询结果。第一次执行该方法,缓存中肯定没有。
- 缓存中不存在该SQL的执行结果,则查询数据库。
查询数据库的操作是通过queryFromDatabase方法完成的。接下来看一下BaseExecutor#queryFromDatabase的具体实现
BaseExecutor#queryFromDatabase
queryFromDatabase;故名思意,该方法是查询数据库返回结果。该方法也是在BaseExecutor中实现的,接下来来看一下它的核心代码(非核心省略)
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
List<E> list;
// 1. 缓存占位
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 2. 又调用doQuery查询
list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
} finally {
// 3. 移除缓存占位
localCache.removeObject(key);
}
// 4. 查询的结果添加到缓存中
localCache.putObject(key, list);
// 省略存储过程相关代码
return list;
}
复制代码
根据代码,我们来简单介绍下queryFromDatabase方法做了哪些事。
- 先把一个占位对象放入到一级缓存中。(有点类似于占座位)。
- 调用doQuery方法执行查询数据库的逻辑(后面重点分析)。
- 完成数据库查询后,从一级缓存中删除占位符。
- 真正的把查询结果缓存到一级缓存中。
该方法的逻辑比较简单,接下来就来看真正执行数据库的方法doQuery吧!
SimpleExecutor#doQuery
doQuery方法的具体实现是交给子类的,我们平时用到的也就是SimpleExecutor,接下来来看下SimpleExecutor是如何实现doQuery方法的
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获取JDBC中的Statement对象
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
// 底层实际上是通过Statement执行SQL的
return handler.query(stmt, resultHandler);
} finally {
// 释放资源
closeStatement(stmt);
}
}
复制代码
我们来简单介绍下该方法的步骤
- 通过Configuration(这是全局配置对象,存储了数据库密码、账户、超时时间等各种配置信息)获取StatementHandler对象。
- 通过prepareStatement方法获取Statement对象。有木有激动!终于看到JDBC中的对象了。有了Statement,我们就可以执行SQL语句。而prepareStatement方法后面会介绍,这里只需要知道它的底层就是connection.createStatement();这种方式来创建Statement的。
- 有了Statement对象后,通过StatementHandler的query方法来处理SQL并返回结果集。
起始这个方法中步骤2和步骤3是最重要的。但是真正执行SQL的逻辑还是在query方法中。query方法是由StatementHandler接口中的方法。StatementHandler的继承体系如下
- PreparedStatementHandler:处理JDBC中的PreparedStatement对象
- SimplePreparedStatement:处理JDBC中的Statement对象
- CallableStatementHandler:处理存储过程
- RoutingStatementHandler:使用了策略者模式,它最后所有的逻辑都委托给上面三个实现类执行
我们只需要关心PreparedStatementHandler SimplePreparedStatement这两个实现类即可
PreparedStatementHandler#query和SimplePreparedStatement#query
上文说到真正执行数据库逻辑的方法是StatementHandler接口中的query方法。并且该接口的两个实现类(PreparedStatementHandler和SimplePreparedStatement)分别实现了JDBC操作中的Statement和PreparedStatement执行SQL的操作。它们的代码比较简单。我就一起贴出来了。代码如下
PreparedStatementHandler#query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
String sql = boundSql.getSql();
statement.execute(sql);
return resultSetHandler.handleResultSets(statement);
}
复制代码
SimplePreparedStatement#query
@Override
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
ps.execute();
return resultSetHandler.handleResultSets(ps);
}
复制代码
可以看到,query方法都执行调用了JDBC的Statement#execute执行SQL。最后也都是由ResultHandler对象处理的结果集并返回。有木有激动,看到了JDBC被封装的代码,起始看到JDBC就已经到了MyBatis的最底层了!到此,MyBatis终于揭开它神秘的面纱。
但是我们仅仅是看到了JDBC中的Statement的身影,这只是MyBatis的冰山一角。MyBatis中还有很多很多多西都值得我们学习,像前文提到的Statement对象究竟是如何获取的,以及最后的ResultHandler是如何处理结果集对象的,都值得我们研究。但是对于初学者来说。到此已经掌握了MyBatis的大致流程。虽然文章标题是具体流程,但是限于篇幅有限,我就粗略的介绍一下了。
不过不要失望,我会继续更新MyBatis源码系列的文章!
相关推荐
- Whoosh,纯python编写轻量级搜索工具
-
引言在许多应用程序中,搜索功能是至关重要的。Whoosh是一个纯Python编写的轻量级搜索引擎库,可以帮助我们快速构建搜索功能。无论是在网站、博客还是本地应用程序中,Whoosh都能提供高效的全文搜...
- 如何用Python实现二分搜索算法(python二分法查找代码)
-
如何用Python实现二分搜索算法二分搜索(BinarySearch)是一种高效的查找算法,适用于在有序数组中快速定位目标值。其核心思想是通过不断缩小搜索范围,每次将问题规模减半,时间复杂度为(O...
- 路径扫描 -- dirsearch(路径查找器怎么使用)
-
外表干净是尊重别人,内心干净是尊重自己,干净,在今天这个时代,应该是一种极高的赞美和珍贵。。。----网易云热评一、软件介绍Dirsearch是一种命令行工具,可以强制获取web服务器中的目录和文件...
- 78行Python代码帮你复现微信撤回消息!
-
来源:悟空智能科技本文约700字,建议阅读5分钟。本文基于python的微信开源库itchat,教你如何收集私聊撤回的信息。...
- 从零开始学习 Python!2《进阶知识》 Python进阶之路
-
欢迎来到Python学习的进阶篇章!如果你说已经掌握了基础语法,那么这篇就是你开启高手之路的大门。我们将一起探讨面向对象编程...
- 白帽黑客如何通过dirsearch脚本工具扫描和收集网站敏感文件
-
一、背景介绍...
- Python之txt数据预定替换word预定义定位标记生成word报告(四)
-
续接Python之txt数据预定替换word预定义定位标记生成word报告(一)https://mp.toutiao.com/profile_v4/graphic/preview?pgc_id=748...
- Python——字符串和正则表达式中的反斜杠('\')问题详解
-
在本篇文章里小编给大家整理的是关于Python字符串和正则表达式中的反斜杠('\')问题以及相关知识点,有需要的朋友们可以学习下。在Python普通字符串中在Python中,我们用'\'来转义某些普通...
- Python re模块:正则表达式综合指南
-
Python...
- python之re模块(python re模块sub)
-
re模块一.re模块的介绍1.什么是正则表达式"定义:正则表达式是一种对字符和特殊字符操作的一种逻辑公式,从特定的字符中,用正则表达字符来过滤的逻辑。(也是一种文本模式;)2、正则表达式可以帮助我们...
- MySQL、PostgreSQL、SQL Server 数据库导入导出实操全解
-
在数字化时代,数据是关键资产,数据库的导入导出操作则是连接数据与应用场景的桥梁。以下是常见数据库导入导出的实用方法及代码,包含更多细节和特殊情况处理,助你应对各种实际场景。一、MySQL数据库...
- Zabbix监控系统系列之六:监控 mysql
-
zabbix监控mysql1、监控规划在创建监控项之前要尽量考虑清楚要监控什么,怎么监控,监控数据如何存储,监控数据如何展现,如何处理报警等。要进行监控的系统规划需要对Zabbix很了解,这里只是...
- mysql系列之一文详解Navicat工具的使用(二)
-
本章内容是系列内容的第二部分,主要介绍Navicat工具的使用。若查看第一部分请见:...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Whoosh,纯python编写轻量级搜索工具
- 如何用Python实现二分搜索算法(python二分法查找代码)
- 路径扫描 -- dirsearch(路径查找器怎么使用)
- 78行Python代码帮你复现微信撤回消息!
- 从零开始学习 Python!2《进阶知识》 Python进阶之路
- 白帽黑客如何通过dirsearch脚本工具扫描和收集网站敏感文件
- Python之txt数据预定替换word预定义定位标记生成word报告(四)
- 假期苦短,我用Python!这有个自动回复拜年信息的小程序
- Python——字符串和正则表达式中的反斜杠('\')问题详解
- Python re模块:正则表达式综合指南
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)