MyBatis 动态 SQL 底层原理分析

我们在使用mybatis的时候,会在xml中编写sql语句。

比如这段动态sql代码:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">

UPDATE users

<trim prefix="SET" prefixOverrides=",">

<if test="name != null and name != ''">

name = #{name}

</if>

<if test="age != null and age != ''">

, age = #{age}

</if>

<if test="birthday != null and birthday != ''">

, birthday = #{birthday}

</if>

</trim>

where id = ${id}

</update>

mybatis底层是如何构造这段sql的?

这方面的知识网上资料不多,于是就写了这么一篇文章。

下面带着这个疑问,我们一步一步分析。

介绍MyBatis中一些关于动态SQL的接口和类

SqlNode接口,简单理解就是xml中的每个标签,比如上述sql的update,trim,if标签:

public interface SqlNode {

boolean apply(DynamicContext context);

}

SqlSource Sql源接口,代表从xml文件或注解映射的sql内容,主要就是用于创建BoundSql,有实现类DynamicSqlSource(动态Sql源),StaticSqlSource(静态Sql源)等:

public interface SqlSource {

BoundSql getBoundSql(Object parameterObject);

}

BoundSql类,封装mybatis最终产生sql的类,包括sql语句,参数,参数源数据等参数:

XNode,一个Dom API中的Node接口的扩展类。

BaseBuilder接口及其实现类(属性,方法省略了,大家有兴趣的自己看),这些Builder的作用就是用于构造sql:

下面我们简单分析下其中4个Builder:

1 XMLConfigBuilder

解析mybatis中configLocation属性中的全局xml文件,内部会使用XMLMapperBuilder解析各个xml文件。

2 XMLMapperBuilder

遍历mybatis中mapperLocations属性中的xml文件中每个节点的Builder,比如user.xml,内部会使用XMLStatementBuilder处理xml中的每个节点。

3 XMLStatementBuilder

解析xml文件中各个节点,比如select,insert,update,delete节点,内部会使用XMLScriptBuilder处理节点的sql部分,遍历产生的数据会丢到Configuration的mappedStatements中。

4 XMLScriptBuilder

解析xml中各个节点sql部分的Builder。

LanguageDriver接口及其实现类(属性,方法省略了,大家有兴趣的自己看),该接口主要的作用就是构造sql:

简单分析下XMLLanguageDriver(处理xml中的sql,RawLanguageDriver处理静态sql):

XMLLanguageDriver内部会使用XMLScriptBuilder解析xml中的sql部分。

ok, 大部分比较重要的类我们都已经介绍了,下面源码分析走起。

源码分析走起

Spring与Mybatis整合的时候需要配置SqlSessionFactoryBean,该配置会加入数据源和mybatis xml配置文件路径等信息:

<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">

<property name="dataSource" ref="dataSource"/>

<property name="configLocation" value="classpath:mybatisConfig.xml"/>

<property name="mapperLocations" value="classpath*:org/format/dao/*.xml"/>

</bean>

我们就分析这一段配置背后的细节:

SqlSessionFactoryBean实现了Spring的InitializingBean接口,InitializingBean接口的afterPropertiesSet方法中会调用buildSqlSessionFactory方法

buildSqlSessionFactory方法内部会使用XMLConfigBuilder解析属性configLocation中配置的路径,还会使用XMLMapperBuilder属性解析mapperLocations属性中的各个xml文件。

部分源码如下:

由于XMLConfigBuilder内部也是使用XMLMapperBuilder,我们就看看XMLMapperBuilder的解析细节。

我们关注一下,增删改查节点的解析。

XMLStatementBuilder的解析:

默认会使用XMLLanguageDriver创建SqlSource(Configuration构造函数中设置)。

XMLLanguageDriver创建SqlSource:

XMLScriptBuilder解析sql:

得到SqlSource之后,会放到Configuration中,有了SqlSource,就能拿BoundSql了,BoundSql可以得到最终的sql。

实例分析

我以以下xml的解析大概说下parseDynamicTags的解析过程:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">

UPDATE users

<trim prefix="SET" prefixOverrides=",">

<if test="name != null and name != ''">

name = #{name}

</if>

<if test="age != null and age != ''">

, age = #{age}

</if>

<if test="birthday != null and birthday != ''">

, birthday = #{birthday}

</if>

</trim>

where id = ${id}

</update>

parseDynamicTags方法的返回值是一个List,也就是一个Sql节点集合。SqlNode本文一开始已经介绍,分析完解析过程之后会说一下各个SqlNode类型的作用。

1 首先根据update节点(Node)得到所有的子节点,分别是3个子节点

(1)文本节点 \n UPDATE users

(2)trim子节点 …

(3)文本节点 \n where id = #{id}

2 遍历各个子节点

(1) 如果节点类型是文本或者CDATA,构造一个TextSqlNode或StaticTextSqlNode

(2) 如果节点类型是元素,说明该update节点是个动态sql,然后会使用NodeHandler处理各个类型的子节点。这里的NodeHandler是XMLScriptBuilder的一个内部接口,其实现类包括TrimHandler、WhereHandler、SetHandler、IfHandler、ChooseHandler等。看类名也就明白了这个Handler的作用,比如我们分析的trim节点,对应的是TrimHandler;if节点,对应的是IfHandler…

这里子节点trim被TrimHandler处理,TrimHandler内部也使用parseDynamicTags方法解析节点

3 遇到子节点是元素的话,重复以上步骤

trim子节点内部有7个子节点,分别是文本节点、if节点、是文本节点、if节点、是文本节点、if节点、文本节点。文本节点跟之前一样处理,if节点使用IfHandler处理

遍历步骤如上所示,下面我们看下几个Handler的实现细节。

IfHandler处理方法也是使用parseDynamicTags方法,然后加上if标签必要的属性。

private class IfHandler implements NodeHandler {

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {

List<SqlNode> contents = parseDynamicTags(nodeToHandle);

MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);

String test = nodeToHandle.getStringAttribute("test");

IfSqlNode ifSqlNode = new IfSqlNode(mixedSqlNode, test);

targetContents.add(ifSqlNode);

}

}

TrimHandler处理方法也是使用parseDynamicTags方法,然后加上trim标签必要的属性。

private class TrimHandler implements NodeHandler {

public void handleNode(XNode nodeToHandle, List<SqlNode> targetContents) {

List<SqlNode> contents = parseDynamicTags(nodeToHandle);

MixedSqlNode mixedSqlNode = new MixedSqlNode(contents);

String prefix = nodeToHandle.getStringAttribute("prefix");

String prefixOverrides = nodeToHandle.getStringAttribute("prefixOverrides");

String suffix = nodeToHandle.getStringAttribute("suffix");

String suffixOverrides = nodeToHandle.getStringAttribute("suffixOverrides");

TrimSqlNode trim = new TrimSqlNode(configuration, mixedSqlNode, prefix, prefixOverrides, suffix, suffixOverrides);

targetContents.add(trim);

}

}

以上update方法最终通过parseDynamicTags方法得到的SqlNode集合如下:

trim节点:

由于这个update方法是个动态节点,因此构造出了DynamicSqlSource。

DynamicSqlSource内部就可以构造sql了:

DynamicSqlSource内部的SqlNode属性是一个MixedSqlNode。

然后我们看看各个SqlNode实现类的apply方法

下面分析一下两个SqlNode实现类的apply方法实现:

MixedSqlNode:

public boolean apply(DynamicContext context) {

for (SqlNode sqlNode : contents) {

sqlNode.apply(context);

}

return true;

}

MixedSqlNode会遍历调用内部各个sqlNode的apply方法。

StaticTextSqlNode:

public boolean apply(DynamicContext context) {

context.appendSql(text);

return true;

}

直接append sql文本。

IfSqlNode:

public boolean apply(DynamicContext context) {

if (evaluator.evaluateBoolean(test, context.getBindings())) {

contents.apply(context);

return true;

}

return false;

}

这里的evaluator是一个ExpressionEvaluator类型的实例,内部使用了OGNL处理表达式逻辑。

TrimSqlNode:

public boolean apply(DynamicContext context) {

FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context);

boolean result = contents.apply(filteredDynamicContext);

filteredDynamicContext.applyAll();

return result;

}

public void applyAll() {

sqlBuffer = new StringBuilder(sqlBuffer.toString().trim());

String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH);

if (trimmedUppercaseSql.length() > 0) {

applyPrefix(sqlBuffer, trimmedUppercaseSql);

applySuffix(sqlBuffer, trimmedUppercaseSql);

}

delegate.appendSql(sqlBuffer.toString());

}

private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) {

if (!prefixApplied) {

prefixApplied = true;

if (prefixesToOverride != null) {

for (String toRemove : prefixesToOverride) {

if (trimmedUppercaseSql.startsWith(toRemove)) {

sql.delete(0, toRemove.trim().length());

break;

}

}

}

if (prefix != null) {

sql.insert(0, " ");

sql.insert(0, prefix);

}

}

}

TrimSqlNode的apply方法也是调用属性contents(一般都是MixedSqlNode)的apply方法,按照实例也就是7个SqlNode,都是StaticTextSqlNode和IfSqlNode。 最后会使用FilteredDynamicContext过滤掉prefix和suffix。

总结

大致讲解了一下mybatis对动态sql语句的解析过程,其实回过头来看看不算复杂,还算蛮简单的。 之前接触mybaits的时候遇到刚才分析的那一段动态sql的时候总是很费解。

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">

UPDATE users

<trim prefix="SET" prefixOverrides=",">

<if test="name != null and name != ''">

name = #{name}

</if>

<if test="age != null and age != ''">

, age = #{age}

</if>

<if test="birthday != null and birthday != ''">

, birthday = #{birthday}

</if>

</trim>

where id = ${id}

</update>

想搞明白这个trim节点的prefixOverrides到底是什么意思(从字面上理解就是前缀覆盖),而且官方文档上也没这方面知识的说明。我将这段xml改成如下:

<update id="update" parameterType="org.format.dynamicproxy.mybatis.bean.User">

UPDATE users

<trim prefix="SET" prefixOverrides=",">

<if test="name != null and name != ''">

, name = #{name}

</if>

<if test="age != null and age != ''">

, age = #{age}

</if>

<if test="birthday != null and birthday != ''">

, birthday = #{birthday}

</if>

</trim>

where id = ${id}

</update>

(第二段第一个if节点多了个逗号) 结果我发现这2段xml解析的结果是一样的,非常迫切地想知道这到底是为什么,然后这也促使了我去看源码的决心。最终还是看下来了。

MyBatis 动态 SQL 底层原理分析相关推荐

  1. MyBatis动态SQL底层原理分析

    前言 废话不多说,直接进入文章. 我们在使用mybatis的时候,会在xml中编写sql语句. 比如这段动态sql代码: <update id="update" parame ...

  2. MyBatis动态SQL底层原理分析 与 JavaScript中的Date对象,以及UTC、GMT、时区的关系...

    http://fangjian0423.github.io/categories/mybatis/ http://xtutu.me/the-date-object-in-js/

  3. Mybatis动态sql是做什么的?都有哪些动态sql?简述一下动态sql的执行原理?

    Mybatis的动态sql的作用: 动态sql就是(在进行sql操作的时候)动态的根据属性值(所匹配的条件)来拼接数据库执行的sql语句,也就是多次查询或变更操作,根据传入的属性值不同,动态拼接出不同 ...

  4. MyBatis 动态 SQL,讲的很细

    点击上方蓝色字体,选择"标星公众号" 优质文章,第一时间送达 整理:阿进的写字台 原文:cnblogs.com/homejim/p/9909657.html MyBatis 令人喜 ...

  5. MyBatis动态SQL语法详解(二)

    文章目录 一.MyBatis查询返回 1.1.MyBatis查询返回对象 1.2.MyBatis查询返回list集合 1.3.MyBatis查询返回Map 1.4.MyBatis查询自定义结果映射规则 ...

  6. (4) hibernate增删查改+批量操作+类似Mybatis动态sql

    简介 采用spring + hibernate + freemaker+ maven搭建起来的一个hibernate增删查改和 类似mybatis动态sql查询的一个案例 增删查改demo + 动态s ...

  7. 一个mybatis动态 SQL查询的完整小案例。包含多表联合查询。

    多表联合查询 一个根据机场查询航线的例子.有两张表,机场表包含机场信息:机场id.机场名字.机场城市. 航班包含航线信息:航班id.飞机编号.飞行时间.票价.起飞机场id.降落机场id. 需要查询的结 ...

  8. MyBatis Dynamic SQL(一)— 真正的Mybatis动态sql — MyBatis Dynamic SQL

    目录 现状 MyBatis Dynamic SQL 项目地址 官方文档 项目介绍 作者 Mybatis Generator 简介 特性 目标 提示 快速使用 步骤 定义常量Tables和Columns ...

  9. Mybatis 动态SQL

    Mybatis 动态SQL 一 .动态SQL 数组 array 使用foreach 标签 <!-- mybatis的集合操作知识点: 如果遇到集合参数传递,需要将集合遍历标签: foreach ...

最新文章

  1. AngularJs详细
  2. DAVINCI DM365-368中 linux-2.6.32的移植
  3. html5环形流程图,环状流程图怎么画好看?5分钟让你精通绘制技巧
  4. python找出矩阵中的最大值_Python / Scipy:找到矩阵的“有界”最小值/最大值
  5. 查询某字段不重复记录的SQL语句
  6. python 获取麦克风声音_python调用pyaudio使用麦克风录制wav声音文件的教程
  7. 【Python秒杀脚本】淘宝或京东等秒杀抢购
  8. 苹果手机投屏win10电脑
  9. matlab海龟交易策略,海龟交易策略
  10. (仿微信Android)IM聊天+抢红包+直播+朋友圈源码发布了
  11. 程序员即装逼又实用的Cmd命令行
  12. DCDC开关电源电磁兼容(二)什么是LISN(有内部电路详细解释)
  13. 智汇华云|安超云套件Archer Cloudsuite为“信创强国”筑基
  14. ubuntu16.04安装opencv3.4.1
  15. 服务的基本使用:定义、启动和停止服务
  16. 李治国回应冯大辉的吐槽:关于口碑网和阿里绕不开的往事
  17. 2022年新一代最强开源UI自动化测试神器(一)
  18. 英国《金融时报》:3G迟来的商机
  19. 加速下载工具motrix
  20. 读书笔记:CSS禅意花园-布局法则

热门文章

  1. PHP无限极分类生成树方法,无限分级
  2. 更换session保存的路径
  3. Hessian局部线性嵌入算法(HLLE)——matlab实现
  4. 238.除自身以外数组的乘积
  5. Network Embedding中方法回顾
  6. 大数——大数相加(hdu1002)
  7. Linux——VIM开发C++自动补全(STL、括号)和开发环境配置
  8. 局部内部类 java 1614958356
  9. final 最终 java 1614876717
  10. 04 理解SQL与T-SQL的概念测试分析 1214