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

简介

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

标签说明

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

if

<if test="title != null">AND title like #{title}
</if>

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

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

choose (when, otherwise)

<choose><when test="title != null">AND title like #{title}</when><when test="author != null and author.name != null">AND author_name like #{author.name}</when><otherwise>AND featured = 1</otherwise>
</choose>

这一系列标签的作用是从多个条件中选择一个使用,类似于代码中 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 id="queryByEmail" resultMap="BaseResultMap">SELECT * FROM `people`WHERE<if test="email != null">AND email = #{email}</if>
</select>
  • 当email字段为null时,生成的语句是 SELECT * FROM people where
  • email不为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 id="queryByEmail" resultMap="BaseResultMap">SELECT * FROM `people`<trim prefix="WHERE" prefixOverrides="AND |OR "><if test="email != null">AND email = #{email}</if></trim>
</select>

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

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

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

where

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

<select id="queryByEmail" resultMap="BaseResultMap">SELECT * FROM `people`<where><if test="email != null">AND email = #{email}</if></where>
</select>

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

set

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

<trim prefix="SET" suffixOverrides=",">...
</trim>

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

foreach

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

<select id="selectPostIn" resultType="domain.blog.Post">SELECT *FROM POST P<where><foreach item="item" index="index" collection="list"open="ID in (" separator="," close=")" nullable="true">#{item}</foreach></where>
</select>

动态Sql配置示例

<select id="queryByAgeAndEmail" resultMap="BaseResultMap">SELECT * FROM `people`<where><if test="age != null">AND age = ${age}</if><if test="email != null">AND email = #{email}</if></where>
</select>

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

List<People> peopleList = mapper.queryByAgeAndEmail(9, "zzz@sample.com");

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

 耗时21 ms sql:SELECT \* FROM `people`WHERE  age = 9AND email = ? param:{age=9, param1=9, email=<zzz@sample.com>, param2=<zzz@sample.com>}

我们已经了解了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相关配置。

    • 此处的动态指 <if|where> 等标签以及 ${} 占位符,但仍可能包含 #{} 占位符  参见 XMLScriptBuilder#parseScriptNode
  • StaticSqlSource:用于描述前几种 sqlSource 解析后得到的静态SQL资源。它们会在参数解析后,最终生成  StaticSqlSource

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

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

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

SqlNode

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

sqlNode.jpg

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

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

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

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

解析sql语句

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

getBoundSql 的代码如下:

 @Overridepublic 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 对象,然后由它生成最终的BoundSqlSqlSource 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语句片段,直接追加到 context
    • TextSqlNode:使用参数替换 ${} 占位符后拼接内容到context
    • WhereSqlNode:拼接WHERE,去除紧跟在后面的 AND |OR   (依赖TrimSqlNode,前文已有说明)
    • IfSqlNode:执行 Ognl 表达式,判断 test 对应的执行结果,true则拼接子节点的内容到context中

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

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

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

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

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

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

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

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

参考

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/15747.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是Docker

容器技术和虚拟机 虚拟机 和一个单纯的应用程序相比&#xff0c;操作系统是一个很重的程序&#xff0c;刚装好的系统还什么都没有部署&#xff0c;单纯的操作系统其磁盘占用至少几十G起步&#xff0c;内存要几个G起步。 在这台机器上开启三个虚拟机&#xff0c;每个虚拟机上…

FFmpeg5.0源码阅读—— avcodec_send_packetavcodec_receive_frame

摘要&#xff1a;本文主要描述了FFmpeg中用于解码的接口的具体调用流程&#xff0c;详细描述了该接口被调用时所作的具体工作。   关键字&#xff1a;ffmpeg、avcodec_send_packet、avcodec_receive_frame   读者须知&#xff1a;读者需要了解FFmpeg的基本使用流程&#xf…

Day48|198.打家劫舍、 213.打家劫舍II 、 337.打家劫舍III

198.打家劫舍 1.题目&#xff1a; 你是一个专业的小偷&#xff0c;计划偷窃沿街的房屋。每间房内都藏有一定的现金&#xff0c;影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统&#xff0c;如果两间相邻的房屋在同一晚上被小偷闯入&#xff0c;系统会自动报警…

UART串口收发数据

uart4.h ubuntuubuntu:05_uart$ cat include/uart4.h #ifndef __UART_H__ #define __UART_H__//初始化相关操作 void hal_uart4_init();//发送一个字符 void hal_put_char(const char str);//发送一个字符串 void hal_put_string(const char* string);//接收一个字符 char h…

如何在没有软件的情况下将 PDF 转换为 PPT(100% 免费)

演示文稿由文字、图片、音频、动画等元素组成&#xff0c;通常用于会议、课堂或演讲中&#xff0c;展示演讲者想要表达的主要内容。如果您遇到重要文档以 PDF 格式存储&#xff0c;但现在需要转换为 PPT 格式的情况&#xff0c;请不要担心。我们本指南的目标是帮助用户将 PDF 转…

一起学SF框架系列5.7-模块Beans-BeanDefinition解析

开发人员按元数据规则定义了应用bean&#xff0c;了解SF如何根据定义解析成BeanDefiniton有助于深入理解框架实现。解析过程如下&#xff1a; 资源加载 从资源文件加载bean的元数据配置&#xff0c;实际过程如下图&#xff1a; 实际从指定的XML文件加载bean定义是从XmlBeanD…

Spring系列2 -- Spring的创建和使用

Spring 就是⼀个包含了众多工具方法的 IOC容器。既然是容器那么它就具备两个最基本的功能&#xff1a; 将对象存储到容器&#xff08;Spring&#xff09;中&#xff1b;从容器中将对象取出来。 在Java中对象也叫做Bean&#xff0c;后续我们就把对象称之为Bean&#xff1b; 目录…

4.5 x64dbg 探索钩子劫持技术

钩子劫持技术是计算机编程中的一种技术&#xff0c;它们可以让开发者拦截系统函数或应用程序函数的调用&#xff0c;并在函数调用前或调用后执行自定义代码&#xff0c;钩子劫持技术通常用于病毒和恶意软件&#xff0c;也可以让开发者扩展或修改系统函数的功能&#xff0c;从而…

ARM64学习笔记---建立异常向量表(二)

源码: #include "mm.h" #include "sysregs.h".section .rodata .align 3 .globl el_string1 el_string1:.string "Booting at EL".section ".text.boot" .globl _start _start://读取mpidr_el1寄存器的值&#xff0c;该寄存器决定了…

高级运维开发工程师带你处理linux木马(挖矿病毒)实战例子

一、事态描述 centos7测试服务器&#xff0c;突然密码登不上去了&#xff0c;然后CPU占100%。已经猜到&#xff0c;登录密码过于简单&#xff0c;密码被破解挂马了。大概率是CPU挖矿病毒。 二、重置centos7登录root密码 步骤1 启动Linux Centos7系统&#xff0c;当出现如下画…

DevOps系列文章 之 Java使用jgit管理git仓库

最近设计基于gitops新的CICD方案,需要通过java读写git仓库&#xff0c;这里简单记录下。 在jgit中&#xff0c;存在最核心的三个组件&#xff1a;Git类&#xff0c;Repository类。Git类中包含了push commit之类的常见git操作&#xff0c;而Repository则实现了仓库的初始化和基…

使用黑盒测试在 Go 中重写 Bash 脚本

目录 前言&#xff1a; 开始 准备工作 描述行为&#xff1a;Bats 简介 行为描述&#xff1a;陷阱 描述行为&#xff1a;设计测试 重写&#xff1a;让我们开始用go吧&#xff01; 重构和更新&#xff1a;实现胜利 结论 前言&#xff1a; 使用黑盒测试在Go中重写Bash脚本…