Mybatis SQL执行过程
ztj100 2025-01-07 17:23 13 浏览 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源码系列的文章!
相关推荐
- Vue 技术栈(全家桶)(vue technology)
-
Vue技术栈(全家桶)尚硅谷前端研究院第1章:Vue核心Vue简介官网英文官网:https://vuejs.org/中文官网:https://cn.vuejs.org/...
- vue 基础- nextTick 的使用场景(vue的nexttick这个方法有什么用)
-
前言《vue基础》系列是再次回炉vue记的笔记,除了官网那部分知识点外,还会加入自己的一些理解。(里面会有部分和官网相同的文案,有经验的同学择感兴趣的阅读)在开发时,是不是遇到过这样的场景,响应...
- vue3 组件初始化流程(vue组件初始化顺序)
-
学习完成响应式系统后,咋们来看看vue3组件的初始化流程既然是看vue组件的初始化流程,咋们先来创建基本的代码,跑跑流程(在app.vue中写入以下内容,来跑流程)...
- vue3优雅的设置element-plus的table自动滚动到底部
-
场景我是需要在table最后添加一行数据,然后把滚动条滚动到最后。查网上的解决方案都是读取html结构,暴力的去获取,虽能解决问题,但是不喜欢这种打补丁的解决方案,我想着官方应该有相关的定义,于是就去...
- Vue3为什么推荐使用ref而不是reactive
-
为什么推荐使用ref而不是reactivereactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代optionapi的data的替...
- 9、echarts 在 vue 中怎么引用?(必会)
-
首先我们初始化一个vue项目,执行vueinitwebpackechart,接着我们进入初始化的项目下。安装echarts,npminstallecharts-S//或...
- 无所不能,将 Vue 渲染到嵌入式液晶屏
-
该文章转载自公众号@前端时刻,https://mp.weixin.qq.com/s/WDHW36zhfNFVFVv4jO2vrA前言...
- vue-element-admin 增删改查(五)(vue-element-admin怎么用)
-
此篇幅比较长,涉及到的小知识点也比较多,一定要耐心看完,记住学东西没有耐心可不行!!!一、添加和修改注:添加和编辑用到了同一个组件,也就是此篇文章你能学会如何封装组件及引用组件;第二能学会async和...
- 最全的 Vue 面试题+详解答案(vue面试题知识点大全)
-
前言本文整理了...
- 基于 vue3.0 桌面端朋友圈/登录验证+60s倒计时
-
今天给大家分享的是Vue3聊天实例中的朋友圈的实现及登录验证和倒计时操作。先上效果图这个是最新开发的vue3.x网页端聊天项目中的朋友圈模块。用到了ElementPlus...
- 不来看看这些 VUE 的生命周期钩子函数?| 原力计划
-
作者|huangfuyk责编|王晓曼出品|CSDN博客VUE的生命周期钩子函数:就是指在一个组件从创建到销毁的过程自动执行的函数,包含组件的变化。可以分为:创建、挂载、更新、销毁四个模块...
- Vue3.5正式上线,父传子props用法更丝滑简洁
-
前言Vue3.5在2024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性...
- Vue 3 生命周期完整指南(vue生命周期及使用)
-
Vue2和Vue3中的生命周期钩子的工作方式非常相似,我们仍然可以访问相同的钩子,也希望将它们能用于相同的场景。...
- 救命!这 10 个 Vue3 技巧藏太深了!性能翻倍 + 摸鱼神器全揭秘
-
前端打工人集合!是不是经常遇到这些崩溃瞬间:Vue3项目越写越卡,组件通信像走迷宫,复杂逻辑写得脑壳疼?别慌!作为在一线摸爬滚打多年的老前端,今天直接甩出10个超实用的Vue3实战技巧,手把...
- 怎么在 vue 中使用 form 清除校验状态?
-
在Vue中使用表单验证时,经常需要清除表单的校验状态。下面我将介绍一些方法来清除表单的校验状态。1.使用this.$refs...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- Vue 技术栈(全家桶)(vue technology)
- vue 基础- nextTick 的使用场景(vue的nexttick这个方法有什么用)
- vue3 组件初始化流程(vue组件初始化顺序)
- vue3优雅的设置element-plus的table自动滚动到底部
- Vue3为什么推荐使用ref而不是reactive
- 9、echarts 在 vue 中怎么引用?(必会)
- 无所不能,将 Vue 渲染到嵌入式液晶屏
- vue-element-admin 增删改查(五)(vue-element-admin怎么用)
- 最全的 Vue 面试题+详解答案(vue面试题知识点大全)
- 基于 vue3.0 桌面端朋友圈/登录验证+60s倒计时
- 标签列表
-
- 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)