Mybatis动态SQL解析:XML配置如何变成最终的Sql语句?

您所在的位置:网站首页 动态sql语句基本语法结构分析 Mybatis动态SQL解析:XML配置如何变成最终的Sql语句?

Mybatis动态SQL解析:XML配置如何变成最终的Sql语句?

2024-07-10 03:30| 来源: 网络整理| 查看: 265

简介

动态SQL是Mybatis的一项核心功能,通过一份静态的XML配置 + 外部参数,动态生成最终的SQL语句,可以用很少的理解成本配置复杂条件的动态SQL,摆脱各种处理逗号、空格这些细枝末节的痛苦。

标签说明

要实现动态拼接SQL,需要在XML中提前配置好相应标签,Mybatis支持以下4类标签:

if AND title like #{title}

if标签的作用是:传入指定参数后,如果 test 表达式执行结果为真,则将   中间包含的内容添加到生成的SQL语句中。

常见的用法是为where子句新增条件。

choose (when, otherwise) AND title like #{title} AND author_name like #{author.name} AND featured = 1

这一系列标签的作用是从多个条件中选择一个使用,类似于代码中 switch、case、default,语义如下

if(title != null){ sql += 'AND title like #{title}'; return; }else if(author != null and author.name != null){ sql += 'AND author_name like #{author.name}'; return; }else{ sql += 'AND featured = 1'; return; } trim (where, set)

if、choose 等标签可以用来解决根据参数动态选择拼接SQL片段的问题,但是只靠这种程度的动态拼接生成的语句基本是不可用的。

举个例子,我现在想按照邮箱查询用户信息表,如果只用if标签,会出现以下情况:

SELECT * FROM `people` WHERE AND email = #{email} 当email字段为null时,生成的语句是 SELECT * FROM people whereemail不为null:生成 SELECT * FROM people where AND email = ?

很明显,这两条SQL语法都是错误的。

trim

为了解决这一问题,Mybatis提供了 trim/where/set 这一系列标签,首先来看trim标签,支持配置以下属性

prefix:前缀prefixesToOverride:前缀后需要被移除的内容,多个值使用 | 分隔suffix:后缀suffixesToOverride:后缀前需要被移除的内容,多个值使用 | 分隔

trim标签的作用是:当子节点生成的内容不为空时, 清除 prefixesToOverride/suffixesToOverride 对应的内容,再拼接上 prefix/suffix 对应的前后缀

我们可以改用trim标签改写按邮箱查询用户的例子

SELECT * FROM `people` AND email = #{email}

trim标签为我们做了以下的事情:

获取子节点内容 email为null:子节点为空(不会进入下一步,对结果无影响)email不null:子节点内容为 AND email = #{email} 字符串 清除 prefixOverrides 对应内容,从 AND email = #{email} 变成 email = #{email} 加上 prefix 对应的前缀 WHERE

所以整个trim 标签执行完后,生成这个结果 WHERE email = ${email}

where

当然,一般来说,这里也用不着trim标签,where用起来会更简单。

SELECT * FROM `people` AND email = #{email}

二者之所以可以实现相同的功能,是因为where是trim指定了特定参数的一种简写形式,二者是等价的。

set

与where类似,set标签也是trim的一种简写形式,对应的参数如下:

...

可以用来动态指定更新哪些字段

foreach

foreach 标签用于对集合元素进行遍历,例如构建 in 条件

SELECT * FROM POST P #{item} 动态Sql配置示例 SELECT * FROM `people` AND age = ${age} AND email = #{email}

写好XML配置后,执行查询语句

List peopleList = mapper.queryByAgeAndEmail(9, "[email protected]");

通过给StatementHandler设置拦截插件,打印出执行的sql语句及参数如下

耗时21 ms sql:SELECT \* FROM `people` WHERE age = 9 AND email = ? param:{age=9, param1=9, email=, param2=}

我们已经了解了Mybatis有哪些动态SQL相关的标签及其作用。接下来,将了解这些是如何实现的。

首先,需要先了解两个概念 SqlSource 和 SqlNode

SqlSource

SqlSource是Mybatis中定义的接口,对应了 通过注解或xml配置的sql语句资源(select|update|insert),有以下4个实现类:

ProviderSqlSource:用于描述通过@Select 等注解配置的SQL

DynamicSqlSource:用于描述Mapper XML文件中配置的SQL

RawSqlSource:用于描述Mapper XML文件中配置的SQL资源信息,不包含动态SQL相关配置。

此处的动态指 等标签以及 ${} 占位符,但仍可能包含 #{} 占位符  参见 XMLScriptBuilder#parseScriptNode

StaticSqlSource:用于描述前几种 sqlSource 解析后得到的静态SQL资源。它们会在参数解析后,最终生成  StaticSqlSource

xml配置信息到 SqlSource 的转换由  LanguageDriver 完成,MyBatis 自带两个实现类

RawLanguageDriver:仅纯sqlXMLLanguageDriver:@Select等注解 和  xml 标签配置的动态sql

还有其他的LanguageDriver,如 Velocity模板 对应 VelocityLanguageDriver (需要额外引入包)

SqlNode

SqlNode是一个接口,用于描述Mapper配置中的某条语句下的节点信息,包含以下的实现类:

sqlNode.jpg

以上文示例中的XML配置为例,初始化时载入这部分配置后,由于包含动态内容,解析并生成了DynamicSqlSource,主要的内容是 SqlNode 节点构造的树状结构。

根节点包含了3个子节点,分别为:

StaticTextSqlNode: 纯文本节点,内容为 SELECT * FROM peopleWhereSqlNode:where节点,包含 3个只有换行/空白字符的纯文本节点 和 两个IF节点StaticTextSqlNode:换行符

更多明细详见下图 配置文件载入SqlSource.png

解析sql语句

调用 mapper.queryByAgeAndEmail() 执行查询时,首先会获取该方法对应的SqlSource,执行 DynamicSqlSource#getBoundSql 这一步获取最终要跑的sql语句。

getBoundSql 的代码如下:

@Override public BoundSql getBoundSql(Object parameterObject) { DynamicContext context = new DynamicContext(configuration, parameterObject); rootSqlNode.apply(context); // 对节点树逐层调用apply,拼接内容到context中 // 此时 context 内容中已经去掉了全部的动态节点 和 ${} 占位符,#{} 还在 SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); // 1. 处理占位符 #{},将其转化为? // 2. 生成 StaticSqlSource 对象,然后由它生成最终的BoundSql SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); BoundSql boundSql = sqlSource.getBoundSql(parameterObject); context.getBindings().forEach(boundSql::setAdditionalParameter); return boundSql; } new DynamicContext(configuration, parameterObject)  准备用于拼接动态sql的上下文,其中包括了参数信息和构造出来的sql语句rootSqlNode.apply(context);  遍历SqlNode节点树,依次调用apply,将对应节点的内容拼接到context中,此处列举部分节点的处理方式: StaticTextSqlNode: 纯静态sql语句片段,直接追加到 contextTextSqlNode:使用参数替换 ${} 占位符后拼接内容到contextWhereSqlNode:拼接WHERE,去除紧跟在后面的 AND |OR   (依赖TrimSqlNode,前文已有说明)IfSqlNode:执行 Ognl 表达式,判断 test 对应的执行结果,true则拼接子节点的内容到context中

会直接附加sql片段到 context 的节点类型如下: appendSql.png

其他类型节点自身不会追加信息,而是遍历子节点时,由对应子节点来添加。

例如 IfSqlNode 会包含一个 StaticTextSqlNode 或 TextSqlNode 子节点(取决于子节点是否包含动态内容),当判断符合条件时,调用 子节点.apply(context)  去实现动态SQL拼接

tips:if 标签开头多余的 AND 是怎么去除的?

WhereSqlNode 指定了 AND|OR  作为前缀需要被覆盖,接着调用父类 TrimSqlNode 的实现

TrimSqlNode 中会生成一个新的临时 context ,存放where下所有子节点的sql片段(也就是说 IfSqlNode里拿到的context 与最外面传进来的context不是同一个)

执行去除前缀后,将临时 context 中的结果拼接到 最外层 context 上

forEach节点也采用了类似的临时context的方式

参考 MyBatis 3源码深度解析-微信读书


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3