MyBatis SqlSource源码示例解析

目录
  • 正文
  • SqlNode
    • SqlNode接口定义
  • BoundSql
  • SqlSource
    • SqlSource解析时机
    • SqlSource调用时机
  • 总结

正文

MyBatis版本:3.5.12。

本篇讲从mybatis的角度分析SqlSource。在xml中sql可能是带?的预处理语句,也可能是带$或者动态标签的动态语句,也可能是这两者的混合语句。

SqlSource设计的目标就是封装xml的crud节点,使得mybatis运行过程中可以直接通过SqlSource获取xml节点中解析后的SQL。

简单的示意图就是

接下来我们先来介绍几个基础的组件,正是这些组件构成的SqlSource

SqlNode

mybatis提供了这么9种动态节点:

  • trim
  • where
  • set
  • foreach
  • if
  • choose
  • when
  • otherwise
  • bind

每一种节点是一个SqlNode,并且每个动态节点都分别对应了一个XxxSqlNode的实现类。SqlNode是一个接口,该接口就代表mybatis的动态节点。

接下来我们来用一个案例分析mybatis是如何把一个<select>节点解析为一个SqlNode对象的(update/insert/delete原理一样)。示例如下

<select id="selectById">
    select * from user
    <where>
        <if test="id != null">
            and id = #{id}
        </if>
        <if test="age != null">
            and age > ${age}
        </if>
    </where>
</select>

它会被解析成如下这样一颗SqlNode树

树的根节点都是MixedSqlNodeMixedSqlNode类其中有一个属性private final List<SqlNode> contents;专门存放标签下所有的子节点解析成的SqlNode

该标签的的第一部分就是select * from user;这段文本既不包含标签,也不包含$等表达式,它就属于静态文本,会被解析成StaticTextSqlNode

  • 然后与接下来是一个wehre标签,它会被解析为WhereSqlNode
  • whhre标签中有两个if标签,这两个if标签会被解析为两个IfSqlNode加入到WhereSqlNode
  • 第一个if标签中的文本不包含$会被解析成StaticTextSqlNode(没错,即使它有#符,它不属于静态文本哦。只有包含$才算动态节点)
  • 而第二个if标签中的文本包含$会被解析成TextSqlNode

看明白了xml文件中一个标签是如何由这些SqlNode是组成的。接下来我们唠一唠SqlNode接口的定义

SqlNode接口定义

public interface SqlNode {
  boolean apply(DynamicContext context);
}

SqlNode接口定义非常简单,只有一个apply方法,方法的参数是DynamicContextDynamicContext可以看作是一个sql上下文,它其中维护了一个StringBuilder sql字段。这个字段就是用来记录整个<select>节点解析过后的SQL语句的。

mybatis会在解析过程中把select标签解析为如上分析的一棵树MixedSqlNode然后就会递归遍历这些SqlNode并调用他们的apply方法,调用apply方法实际上就是把标签解析后的sql片段拼接到了context中的sql字段。最后只需要调用context.getSql方法就可以获得可执行SQL了。而一切都从根节点的apply方法说起,MixedSqlNode的源码如下

public class MixedSqlNode implements SqlNode {
  private final List<SqlNode> contents;
  public MixedSqlNode(List<SqlNode> contents) {
    this.contents = contents;
  }
  @Override
  public boolean apply(DynamicContext context) {
    contents.forEach(node -> node.apply(context));
    return true;
  }
}

可以发现MixedSqlNode中有一个List字段,该字段存储的是树的叶子节点,在这个示例中,List字段中应该由两个SqlNode

  • 第一个是标识静态文本的StaticTextSqlNode,它其中封装的select * from user文本。
  • 第二个SqlNode是WhereSqlNode它其中封装的文本是
  <where>
      <if test="id != null">
          and id = #{id}
      </if>
      <if test="age != null">
          and age > ${age}
      </if>
  </where>

WhereSqlNode类中也还有一个List属性,封装了两个if节点,这里就不展开说了,我们只需要知道,所有的SqlNode都会递归执行apply方法,而apply方法只做了一件事——那就是把SqlNode节点中的文本经过一系列规则解析过后(通常就是删除标签,删除无用的and|or,删除无用的,等),返回可执行SQL的片段,这些SQL片段最终都会以如下方法把sql片段拼接,

context.appendSql(text);

最终形成一个完整的SQL:select * from user where id = 1 (age条件没成立)

BoundSql

知道了什么是SqlNode之后,我们再来看BoundSqlBoundSql内部封装了可执行SQL,先来看下BoundSql的重要字段

public class BoundSql {
  private final String sql;
  private final List<ParameterMapping> parameterMappings;
  private final Object parameterObject;
  private final Map<String, Object> additionalParameters;
  private final MetaObject metaParameters;
}
  • sql:上小节说到的SqlNode调用完apply方法后存储在DynamicContext中的sql就会被赋值给该字段。sql字段其实就是类似于select * from user where id = ? 这样的字符串,
  • parameterObject:用户传入的属性,用于给sql字段的?赋值
  • additionalParameters: bind标签中绑定的值会存储在此
  • metaParameters:additionalParameters的元类型

还记得开篇我们说的目标吗?我贴过来再看一遍

SqlSource设计的目标就是封装xml的crud节点,使得mybatis运行过程中可以直接通过SqlSource获取xml节点中解析后的SQL。

简单的示意图就是

那么有了BoundSql,实现这个目标是不是就很容易了。我们只需要获取BoundSql对象,然后再调用BoundSql#getSql方法就能获取到可执行Sql了。

SqlSource

为了完成开篇说的SqlSource的目标,我们现在迫切想要做的就是获取BoundSql对象。刚好SqlSource接口的定义如下

public interface SqlSource {
  BoundSql getBoundSql(Object parameterObject);
}

SqlSource是一个接口,其中只提供了一个方法 getBoundSql 。该方法只有一个参数Object parameterObject,这个参数就是用户传入的查询参数。SqlSource的继承体系如下

  • DynamicSqlSource:动态SQL节点会被解析为该对象,那怎么判断xml文件中的节点是否是动态的呢?满足如下两个条件的任何一个就算是动态节点。一是包含$占位符的表达式,比如select * from user where id = ${id}。二是包含9种动态标签中的任何一个(trim set wehre if foreach等9个。前文有说)。注意只包含#占位符表达式的语句不会被解析成动态标签。
  • ProviderSqlSource:注解定义的SQL
  • RawSqlSource:不是DynamicSqlSource,就会被解析为RawSqlSource
  • . StaticSqlSource:静态文本SQL其中不包含任何$和动态标签。DynamicSqlSource和RawSqlSource最终都会被解析为StaticSqlSource
  • . VelocitySqlSource:暂且忽略(不在本文讨论范围)

SqlSource解析时机

至此SqlSource的组成部分我们都已经清楚了,那么XML的节点在何时被解析为SqlSource的呢?

答案是在mybatis启动时,会加载xml文件并进行解析。相关流程如下

  • XMLMapperBuilder#configurationElement,
private void configurationElement(XNode context) {
  try {
    String namespace = context.getStringAttribute("namespace");
    if (namespace == null || namespace.isEmpty()) {
      throw new BuilderException("Mapper's namespace cannot be empty");
    }
    builderAssistant.setCurrentNamespace(namespace);
    cacheRefElement(context.evalNode("cache-ref"));
    cacheElement(context.evalNode("cache"));
    parameterMapElement(context.evalNodes("/mapper/parameterMap"));
    resultMapElements(context.evalNodes("/mapper/resultMap"));
    sqlElement(context.evalNodes("/mapper/sql"));
    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
  } catch (Exception e) {
    throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
  }
}

该方法解析一个xml文件中所有的节点:namespace、cache-ref、cache等,其中解析select|insert|update|delete节点的方法是buildStatementFromContext

  • XMLMapperBuilder#buildStatementFromContext
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
  for (XNode context : list) {
    final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
    try {
      statementParser.parseStatementNode();
    } catch (IncompleteElementException e) {
      configuration.addIncompleteStatement(statementParser);
    }
  }
}

该方法会遍历xml中的所有select|insert|update|delete节点并解析。其中list标识所有select|insert|update|delete节点的结合。接下来来看parseStatementNode这个方法,它用来解析单个select|insert|update|delete节点

  • XMLStatementBuilder#parseStatementNode
public void parseStatementNode() {
  // 省略解析 id flushCache useCache SelectKey resultType等属性的过程
  // 创建SqlSource对象,也就是解析xml的crud标签,封装成SqlSource对象,然后再把SqlSource对象存入MS对象中
  SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
  builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
      fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
      resultSetTypeEnum, flushCache, useCache, resultOrdered,
      keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets, dirtySelect);
}

可以看到SqlSource在此被创建了,并且最后作为MappedStatement的属性存储在MappedStatement对象中。这里我们着重关心SqlSource的创建过程,它是在createSqlSource方法完成的

  • XMLLanguageDriver#createSqlSource
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
  XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
  return builder.parseScriptNode();
}

XMLLanguageDriver又委托XMLScriptBuilder解析,接下来我们看XMLScriptBuilder#parseScriptNode方法

  • XMLScriptBuilder#parseScriptNode
public SqlSource parseScriptNode() {
  MixedSqlNode rootSqlNode = parseDynamicTags(context);
  SqlSource sqlSource;
  if (isDynamic) {
    sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
  } else {
    sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
  }
  return sqlSource;
}

mybatis就是在这个方法中创建SqlSource对象,他首先会调用parseDynamicTags方法来解析下节点是否是动态节点,它的解析过程就是看节点是否包含动态标签或包含$占位符,如果满足任意一个条件它就会被解析为动态标签,并创建DynamicSqlSource对象,否则创建RawSqlSource对象

SqlSource调用时机

mybatis需要的是可以执行的SQL,而通过SqlSource我们可以获取BoundSql进而获取BoundSql中的sql字段(该字段就是可执行语句)。所以其调用时机是在mybatis进行查询数据库的时候——调用SqlSource#getBoundSql

具体代码处是Executor执行query方法的时候调用,源码在BaseExecutor中,BaseExecutor#query代码如下

public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
  BoundSql boundSql = ms.getBoundSql(parameter);
  CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
  return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public BoundSql getBoundSql(Object parameterObject) {
  BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
  return boundSql;
}

我们前文说过,SqlSource生成时会被存储在MappedStatement对象当中,所以这里自然也是通过MappedStatement对象来使用SqlSource获取BoundSql。这样在mybatis真正调用JDBC查询数据库的时候就可以通过BoundSql拿到可执行语句啦

总结

  • SqlSource封装了XML中的select|insert|update|delete节点,每个节点都会被解析为MixedSqlNode,可以看作是一棵树,其中包含许多子节点嵌套
  • 只包含#的sql不算动态节点,只有包含动态标签或者$占位符才算是动态节点
  • BoundSql中包含了可执行sql

本文只是粗略的介绍了SqlSource,只能带你粗略的了解下mybatis的组件结构。其中SqlSource如何获取BoundSql对象,以及节点到底是如何被解析的,比如if标签是如何进行判断的 等。读者在理解了这些概念后再阅读源码会容易很多。

以上就是MyBatis SqlSource源码示例解析的详细内容,更多关于MyBatis SqlSource源码解析的资料请关注我们其它相关文章!

(0)

相关推荐

  • 详解MyBatis ResultSetHandler 结果集的解析过程

    目录 正文 ResultSetHandler#handleResultSets 第一部分:ResultSetWrapper 第二部分:验证rsw对象 第三部分:遍历rsw中的结果集 第四部分:处理ResultSets标签 第五部分:collapseSingleResultList 总结 正文 mybatis版本:3.5.12 mybatis通过Executor查询出结果后,通常返回的是一个List结构,再根据用户调用的API把List结构转为指定结构. 比如用户调用SqlSession#sele

  • Mybatis TypeHandler接口及继承关系示例解析

    目录 开篇 TypeHandler接口 TypeHandler继承体系 IntegerTypeHandler DateTypeHandler TypeHandlerRegistry TypeHandlerRegistry#register方法 总结 开篇 JDBC类型与Java类型并不是完全一一对应的.所以在PreparedStatement绑定参数的时候需要把Java类型转为JDBC类型.JDBC类型的枚举值在JdbcType枚举值中存储. MyBatis中提供了一个接口专用于JDBC类型与J

  • 更简单更高效的Mybatis Plus最新代码生成器AutoGenerator

    目录 正文 一.概述 二.使用AutoGenerator 1. 初始化数据库表结构(以User用户表为例) 2. 在 pom.xml 文件中添加 AutoGenerator 的依赖. 3. 添加模板引擎依赖 4. 全局配置 5. 自定义模板生成DTO.VO User用户类 总结 正文 MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发.提高效率而生. 今天的主角是MP推出的一款代码生成器,本文主要来介绍一下它强大的代

  • MyBatis的MapKey注解实例解析

    目录 使用 一.数据准备 二.Mapper配置 UserMapper接口 三.实战 实战2——注意事项 原理 总结 使用 mybatis中有很多实用的注解,但是平时想不起来使用.今天就来讲一下MapKey是如何使用的 说明:本文基于mybatis原生框架3.3.0-SNAPSHOT 一.数据准备 数据库准备一张user表,插入一点测试数据 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(

  • MyBatis SqlSource源码示例解析

    目录 正文 SqlNode SqlNode接口定义 BoundSql SqlSource SqlSource解析时机 SqlSource调用时机 总结 正文 MyBatis版本:3.5.12. 本篇讲从mybatis的角度分析SqlSource.在xml中sql可能是带?的预处理语句,也可能是带$或者动态标签的动态语句,也可能是这两者的混合语句. SqlSource设计的目标就是封装xml的crud节点,使得mybatis运行过程中可以直接通过SqlSource获取xml节点中解析后的SQL.

  • Flink 侧流输出源码示例解析

    目录 Flink 侧流输出源码解析 源码解析 TimestampedCollector#collect CountingOutput#collect BroadcastingOutputCollector#collect RecordWriterOutput#collect ProcessOperator#ContextImpl#output CountingOutput#collect BroadcastingOutputCollector#collect RecordWriterOutput

  • JS前端操作 Cookie源码示例解析

    目录 引言 源码分析 使用 源码 分析 set get remove withAttributes & withConverter 总结 引言 前端操作Cookie的场景其实并不多见,Cookie也因为各种问题被逐渐淘汰,但是我们不用Cookie也可以学习一下它的思想,或者通过这次的源码来学习其他的一些知识. 今天带来的是:js-cookie 源码分析 使用 根据README,我们可以看到js-cookie的使用方式: // 设置 Cookies.set('name', 'value'); //

  • OpenMP task construct 实现原理及源码示例解析

    目录 前言 从编译器角度看 task construct Task Construct 源码分析 总结 前言 在本篇文章当中主要给大家介绍在 OpenMP 当中 task 的实现原理,以及他调用的相关的库函数的具体实现. 在本篇文章当中最重要的就是理解整个 OpenMP 的运行机制. 从编译器角度看 task construct 在本小节当中主要给大家分析一下编译器将 openmp 的 task construct 编译成什么样子,下面是一个 OpenMP 的 task 程序例子: #inclu

  • Flutter加载图片流程之ImageProvider源码示例解析

    目录 加载网络图片 ImageProvider resolve obtainKey resolveStreamForKey loadBuffer load(被废弃) evict 总结 困惑解答 加载网络图片 Image.network()是Flutter提供的一种从网络上加载图片的方法,它可以从指定的URL加载图片,并在加载完成后将其显示在应用程序中.本节内容,我们从源码出发,探讨下图片的加载流程. ImageProvider ImageProvider是Flutter中一个抽象类,它定义了一种

  • Python 装饰器常用的创建方式及源码示例解析

    目录 装饰器简介 基础通用装饰器 源码示例 执行结果 带参数装饰器 源码示例 源码结果 源码解析 多装饰器执行顺序 源码示例 执行结果 解析 类装饰器 源码示例 执行结果 解析 装饰器简介 装饰器(decorator)是一种高级Python语法.可以对一个函数.方法或者类进行加工.在Python中,我们有多种方法对函数和类进行加工,相对于其它方式,装饰器语法简单,代码可读性高.因此,装饰器在Python项目中有广泛的应用.修饰器经常被用于有切面需求的场景,较为经典的有插入日志.性能测试.事务处理

  • React Refs 的使用forwardRef 源码示例解析

    目录 三种使用方式 1. String Refs 2. 回调 Refs 3. createRef 两种使用目的 Refs 转发 createRef 源码 forwardRef 源码 三种使用方式 React 提供了 Refs,帮助我们访问 DOM 节点或在 render 方法中创建的 React 元素. React 提供了三种使用 Ref 的方式: 1. String Refs class App extends React.Component { constructor(props) { su

  • Flutter加载图片流程之ImageCache源码示例解析

    目录 ImageCache _pendingImages._cache._liveImages maximumSize.currentSize clear evict _touch _checkCacheSize _trackLiveImage putIfAbsent clearLiveImages 答疑解惑 ImageCache const int _kDefaultSize = 1000; const int _kDefaultSizeBytes = 100 << 20; // 100 M

  • 【MyBatis源码全面解析】MyBatis一二级缓存介绍

    MyBatis缓存 我们知道,频繁的数据库操作是非常耗费性能的(主要是因为对于DB而言,数据是持久化在磁盘中的,因此查询操作需要通过IO,IO操作速度相比内存操作速度慢了好几个量级),尤其是对于一些相同的查询语句,完全可以把查询结果存储起来,下次查询同样的内容的时候直接从内存中获取数据即可,这样在某些场景下可以大大提升查询效率. MyBatis的缓存分为两种: 一级缓存,一级缓存是SqlSession级别的缓存,对于相同的查询,会从缓存中返回结果而不是查询数据库 二级缓存,二级缓存是Mapper

  • oracle数据与文本导入导出源码示例

    oracle提供了sqlldr的工具,有时需要讲数据导入到文本,oracle的spool可以轻松实现. 方便的实现oracle导出数据到txt.txt导入数据到oracle. 一.导出数据到txt 用all_objects表做测试 SQL> desc all_objects; Name Null? Type ----------------------------------------- -------- ---------------------------- OWNER NOT NULL

随机推荐