Mybatis SQL执行过程

开篇

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作用
SqlSessionSqlSession提供了CRUD的方法,通过调用select/update/insert/delete方法(参数是SQL存储位置),就可以完成对数据库的查询
Executor执行器;SqlSession中的内部属性。SqlSession会委托Executor来执行SQL语句。还包括一些复杂操作。比如缓存等,就是在Executor中完成的。
StatementHandlerSqlSession的方法参数并不是直接的SQL,而是SQL存储的位置。那么Executor执行的SQL语句就是由StatementHandler根据指定位置解析出来的。
ResultSetHandlerResultSetHandler;用来处理结果集对象。比如Select user_name from user; 其中结果集中的user_name和实体User的userName属性关联,就是由ResultSetHandler完成的
TypeHandlerJDBC类型——Java类型的转换
ParameterHandlerPreparedStatement的参数设置

交接了大概执行步骤后,接下来带着这个思路看源码。

具体流程

我们借用开篇的示例来看一下

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方法)**

它有接口体系如下

看源码的过程中(注意是看源码的过程中哦)最常用的是SimpleExecutorBaseExecutor。我们只需要关注这两个类的方法实现就好了。而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源码系列的文章!

作者:念念清晰
原文链接:https://juejin.cn/post/7196975187992002597
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值