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

news2024/9/19 10:38:57

简介

动态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 = 9
                AND 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 的代码如下:

 @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语句片段,直接追加到 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.coloradmin.cn/o/736906.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

C++菱形继承

菱形继承 菱形继承&#xff1a;指有一个基类被两个不同的类所继承&#xff0c;且存在一个类继承于这两个类而形成的一种菱形关系&#xff0c;故称菱形继承。如下图所示&#xff1a; 菱形继承存在的问题 二义性&#xff08;ambiguity&#xff09;&#xff1a;当派生类同时继…

memset(addr , 0 , size) 导致Bus error问题分析

导致问题出现的demo代码 #define SH_DEV_MEM "/dev/mem" #define myerror printf // 获取由mmap映射的设备物理内存 static void *mymmap(u32 offset, u32 size, u8 is_rd_only, u8 is_clear) {void *ptr;s32 fd;offset 0x45E00000;size 0x1000;is_rd_only 0;is_…

【Step By Step】VM安装redhat-server7.9搭建Oracle19C RAC(一)环境配置

文章目录 环境规划网络规划文件系统规划rac用户规划grid与oracle用户规划ASM规划 虚拟机设置搭建虚拟机自定义网卡安装操作系统 操作系统设置关闭services修改/etc/hosts创建用户与组创建文件目录设置环境变量设置内核参数资源限制添加 etc/pam.d/login关闭大页关机挂载本地ISO…

【C++初阶】:vector的基本介绍

vector的基本介绍 一.vector(向量)介绍二.vector原型三.构造四.区别reserve和resize五.二维数组 一.vector(向量)介绍 二.vector原型 vector主要分为两个模板参数&#xff0c;一个是T&#xff0c;T是将数据类型进行实例化&#xff08;本质就是一个数组&#xff09;。第二个参数…

idea 搭建 SpringBoot 集成 mybatis

编译器&#xff1a;IDEA 环境&#xff1a;win10&#xff0c;jdk1.8&#xff0c;maven3.5 数据库&#xff1a;mysql 5.7 一、打开IDEA新建项目 1. 如果你是第一次使用IDEA&#xff0c;那么你需要配置你本地的maven&#xff0c;点击右下角 Configure&#xff0c;如已配置请忽…

第三章:Linux简介及面试常问问题

目录 一、Linux发展史 1.Linux前身-Unix 2.Linux诞生 3.开源文化 4.Linux系统特点 5.Linux操作系统优点 6.Linux操作系统发行版 7.Linux内核命名规则 二、Linux系统的安装 三、linux系统分区简介及如何分区 1.自动分区 2.手动分区 3.linux下查看分区信息和剩余空间…

STM32CubeMX安装

一、配置JAVA环境 1.相关资料链接 链接: CubeMX 提取码&#xff1a;13ec 双击改exe文件。 2.更改默认的安装路径 3.在该exe文件同等目录下&#xff0c;新建一个JAVA-Enviroment文件夹。 选择更改安装路径&#xff0c;放在刚才新建的JAVA-Enviroment文件夹中。 4.等待安装完…

RabbitMQ - 单机部署(超详细)

RabbitMQ部署 1.单机部署 我们在Centos7虚拟机中使用Docker来安装。 1.1.下载镜像 方式一&#xff1a;在线拉取 docker pull rabbitmq:3-management方式二&#xff1a;从本地加载 也可以从网上搜索 RabbitMQ 的 tar 包下载下来 上传到虚拟机中后&#xff0c;使用命令加载…

2023高校夏令营 | 第七年,我们又开营啦!

进入7月&#xff0c;成都的天气就不再像之前那样温和&#xff0c;迎接我们的是热浪滔天&#xff0c;以及一场场猝不及防的高温暴雨。如同当下的毕业季&#xff0c;脱离校园庇护的应届生们涌入就业市场&#xff0c;开始应对来自社会、职场的各种风雨考验。 为了加快推进产教融合…

js深拷贝、js使用递归实现深拷贝

相信看到这篇文章之前你应该多多少少了解过什么是深拷贝了为什么要什么拷贝了 这里就不介绍什么是深拷贝、浅拷贝了 最优解深拷贝方法&#xff1a;递归 这里为了方便直接在vue的页面里面演示了&#xff0c;在其他js文件中使用注意下this mounted() {const obj {str: "字…

实战解决百度旋转验证码

1、效果演示 2、如何识别 2.1准备数据集 首先需要使用爬虫&#xff0c;对验证码图片进行采集&#xff0c;尽量每一种类型都要采集到。 2.2图像矫正 接下来对采集的数据进行人工校正 2.3数据清洗 &#xff08;1&#xff09;对数据进行进行旋转&#xff0c;达到增加数据量的目…

44. 日期交叉问题

文章目录 题目需求思路一实现一原理二实现二学习链接题目来源 题目需求 现有各品牌优惠周期表&#xff08;promotion_info&#xff09;如下&#xff0c;其记录了每个品牌的每个优惠活动的周期&#xff0c;其中同一品牌的不同优惠活动的周期可能会有交叉。 现要求统计每个品牌…

聊一聊Java中的Steam流 | 京东物流技术团队

1 引言 在我们的日常编程任务中&#xff0c;对于集合的制造和处理是必不可少的。当我们需要对于集合进行分组或查找的操作时&#xff0c;需要用迭代器对于集合进行操作&#xff0c;而当我们需要处理的数据量很大的时候&#xff0c;为了提高性能&#xff0c;就需要使用到并行处…

Golang内存分配及垃圾回收

为什么需要垃圾回收&#xff1f; 自动释放不需要的对象&#xff0c;让出存储器资源&#xff0c;无需程序员手动执行 Go V1.3之前是标记-清除算法 具体步骤 缺点&#xff1a;程序卡顿、扫描整个heap、数据清除会产生heap碎片 V1.3之后&#xff0c;做了简单的优化 V1.5之后&a…

AIFORE Smart Fuzzing Based on Automatic Input Format Reverse Engineering

AIFORE: Smart Fuzzing Based on Automatic Input Format Reverse Engineering 1 中国科学院信息工程研究所2清华大学网络科学与网络空间研究院;中关村实验室3华为技术有限公司奇异安全实验室4中国科学院大学网络安全学院5中国人民大学6洛桑联邦理工学院 论文链接 &#xff1a…

PHP实战开发26-使用PHP生成图片验证码并进行校验

文章目录 一、前言二、什么是验证码&#xff1f;三、PHP生成验证码图像3.1 生成验证码文字3.2 创建图像并绘制验证码文字3.3 将验证码存储到Session中 四、前端使用jQuery验证输入4.1 步骤一&#xff1a;创建HTML结构和CSS样式4.2 使用jQuery发送AJAX请求进行校验4.3 步骤三&am…

Head Pose Estimation头部姿态估计任务的基本了解:从数据集开始

目录 前言一、HPE是什么&#xff1f;二、常用数据集1.COFW2.WFLW3.AFLW2000-3D4.300W-LP 三、SOTA workCVPR2022:SynergyNetCVPR2021 Workshop:ASMNet 四、我们想做的贡献 前言 为了检测司机的疲劳状态&#xff0c;除了基于人脸关键点的检测去判断是否闭眼&#xff0c;是否打哈…

基础算法-差分

差分其实就是求前缀和的逆运算 差分数组&#xff1a; Step1 首先给定一个原数组a&#xff1a;a[1], a[2], a[3],,,,,, a[n]; 然后我们构造一个数组b &#xff1a; b[1] ,b[2] , b[3],,,,,, b[i]; 使得 a[i] b[1] b[2 ] b[3] ,,,,,, b[i] 也就是说&#xff0c;a数组是b…

nvm升级node版本

1.首先有安装nvm和配置环境的前提 2、命令查看&#xff0c;切换到D盘目录下 3、安装到指定的版本 nvm安装指定版本的node 输入&#xff1a;nvm install 16.18.1 4、查看已有的node版本 nvm list 5、项目终端查看

【uniapp开发小程序】实现点击跳转手机通话 拨打电话功能

效果图&#xff1a; 代码展示&#xff1a; <template><view class"page-map"><view class"btn" click"telFun()" style"text-align: center;">电话咨询</view></view> </template> <script&g…