百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

Mybatis SQL执行过程

ztj100 2025-01-07 17:23 13 浏览 0 评论

开篇

mybatis版本:3.5.12

JDBC视角看数据库操作

我们知道,MyBatis是对JDBC的封装。让程序员从复杂繁琐的JDBC编程中解放双手。简单回顾一下JDBC编程的过程。通常分为这么几步:

  1. 加载驱动
  2. 获取连接Connection对象
  3. 从Connection中获取Statement对象(或者是PreparedStatement)对象
  4. 用户准备SQL语句
  5. 使用Statement对象执行SQL语句获得结果集对象——ResultSet
  6. 解析ResultSet对象,从ResultSet中获取需要的值。
  7. 关闭资源

以上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需要经过这么几个步骤

  1. 读取数据库配置(账号、密码、文件资源位置等)
  2. 获取会话对象(SqlSession,相当于一个命令行黑窗口界面。可以操作SQL语句)
  3. 执行用户提供的SQL并返回结果集(包含Mapper方式)
  4. 自动释放资源

下面是代码描述

// 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方法只做了两件事

  1. 通过配置对象获取MappedStatement对象,MappedStatement中包含了解析后的SQL语句。(ms对象的具体实现现在不必关心,只需要知道它其中分装好了SQL即可)为了方便理解,不妨给他起个名字——后面我们就称它为sql包装对象
  2. 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方法的大致执行逻辑是

  1. sql包装对象(MappedStatement) 中获取真实的SQL
  2. 根据sql和一系列参数创建缓存的key值。该key值能在一级缓存中唯一确定一个对象。
  3. 根据CacheKey(缓存key)从一级缓存中获取查询结果。第一次执行该方法,缓存中肯定没有。
  4. 缓存中不存在该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方法做了哪些事。

  1. 先把一个占位对象放入到一级缓存中。(有点类似于占座位)。
  2. 调用doQuery方法执行查询数据库的逻辑(后面重点分析)。
  3. 完成数据库查询后,从一级缓存中删除占位符。
  4. 真正的把查询结果缓存到一级缓存中。

该方法的逻辑比较简单,接下来就来看真正执行数据库的方法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);
  }
}
复制代码

我们来简单介绍下该方法的步骤

  1. 通过Configuration(这是全局配置对象,存储了数据库密码、账户、超时时间等各种配置信息)获取StatementHandler对象。
  2. 通过prepareStatement方法获取Statement对象。有木有激动!终于看到JDBC中的对象了。有了Statement,我们就可以执行SQL语句。而prepareStatement方法后面会介绍,这里只需要知道它的底层就是connection.createStatement();这种方式来创建Statement的。
  3. 有了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...

取消回复欢迎 发表评论: