关于java:Mybatis系列全解八Mybatis的9大动态SQL标签你知道几个提前致女神

8次阅读

共计 32080 个字符,预计需要花费 81 分钟才能阅读完成。

封面:洛小汐

作者:潘潘

2021 年,俯视天空,好高鹜远。

这算是春节后首篇 Mybatis 文了~

跨了个年感觉写了有半个世纪 …

借着女神节 ヾ(◍°∇°◍)ノ゙

提前祝男神女神们越靓越富越嗨森!

上图保留可做朋友圈封面图 ~

前言

本节咱们介绍 Mybatis 的弱小个性之一:动静 SQL,从动静 SQL 的诞生背景与根底概念,到动静 SQL 的标签成员及根本用法,咱们徐徐道来,再联合框架源码,分析动静 SQL(标签)的底层原理,最终在文末吐槽一下:在无动静 SQL 个性(标签)之前,咱们会经常掉进哪些可恶的坑吧~

倡议关注咱们!Mybatis 全解系列始终在更新哦

Mybaits 系列全解

  • Mybatis 系列全解(一):手写一套长久层框架
  • Mybatis 系列全解(二):Mybatis 简介与环境搭建
  • Mybatis 系列全解(三):Mybatis 简略 CRUD 应用介绍
  • Mybatis 系列全解(四):全网最全!Mybatis 配置文件 XML 全貌详解
  • Mybatis 系列全解(五):全网最全!详解 Mybatis 的 Mapper 映射文件
  • Mybatis 系列全解(六):Mybatis 最硬核的 API 你晓得几个?
  • Mybatis 系列全解(七):Dao 层的两种实现之传统与代理
  • Mybatis 系列全解(八):Mybatis 的动静 SQL
  • Mybatis 系列全解(九):Mybatis 的简单映射
  • Mybatis 系列全解(十):Mybatis 注解开发
  • Mybatis 系列全解(十一):Mybatis 缓存全解
  • Mybatis 系列全解(十二):Mybatis 插件开发
  • Mybatis 系列全解(十三):Mybatis 代码生成器
  • Mybatis 系列全解(十四):Spring 集成 Mybatis
  • Mybatis 系列全解(十五):SpringBoot 集成 Mybatis
  • Mybatis 系列全解(十六):Mybatis 源码分析

本文目录

1、什么是动静 SQL

2、动静 SQL 的诞生记

3、动静 SQL 标签的 9 大标签

4、动静 SQL 的底层原理

1、什么是动静 SQL?

对于动静 SQL,容许咱们了解为“动静的 SQL”,其中“动静的”是形容词,“SQL”是名词,那显然咱们须要先了解名词,毕竟形容词仅仅代表它的某种状态或者某种状态。

SQL 的全称是:

Structured Query Language,结构化查询语言。

SQL 自身好说,咱们小学时候都学习过了,无非就是 CRUD 嘛,而且咱们还晓得它是一种 语言 ,语言是一种存在于对象之间用于交换表白的 能力,例如跟中国人交换用汉语、跟英国人交换用英语、跟火星人交换用火星语、跟小猫交换用喵喵语、跟计算机交换咱们用机器语言、跟数据库管理系统(DBMS)交换咱们用 SQL。

想必大家立马就能明确,想要与某个对象交换,必须领有与此对象交换的语言能力才行!所以无论是技术人员、还是应用程序零碎、或是某个高级语言环境,想要拜访 / 操作数据库,都必须具备 SQL 这项能力;因而你能看到像 Java,像 Python,像 Go 等等这些高级语言环境中,都会嵌入(反对)SQL 能力,达到与数据库交互的目标。

很显然,可能学习 Mybatis 这么一门高精尖(ru-men)长久层框架的编程人群,对于 SQL 的编写能力必定曾经把握得 ss 的,平时各种 SQL 编写那都是信手拈来的事,只不过对于 动静 SQL 到底是个什么货色,仿佛还有一些敌人似懂非懂!然而没关系,咱们百度一下。

动静 SQL:个别指依据用户输出或内部条件 动静组合 的 SQL 语句块。

很容易了解,随内部条件动静组合的 SQL 语句块!咱们先针对动静 SQL 这个词来分析,世间万物,有动静那就绝对应的有动态,那么他们的边界在哪里呢?又该怎么辨别呢?

其实,下面咱们曾经介绍过,在例如 Java 高级语言中,都会嵌入(反对)SQL 能力,个别咱们能够间接在代码或配置文件中编写 SQL 语句,如果一个 SQL 语句在“编译阶段”就曾经能确定 主体构造,那咱们称之为动态 SQL,如果一个 SQL 语句在编译阶段无奈确定主体构造,须要等到程序真正“运行时”能力最终确定,那么咱们称之为动静 SQL,举个例子:

<!-- 1、定义 SQL -->
<mapper namespace="dao">
    <select id="selectAll" resultType="user">
        select * from t_user
    </select>
</mapper>
// 2、执行 SQL
sqlSession.select("dao.selectAll");

很显著,以上这个 SQL,在编译阶段咱们都曾经晓得它的主体构造,即查问 t_user 表的所有记录,而无需等到程序运行时才确定这个主体构造,因而以上属于 动态 SQL。那咱们再看看上面这个语句:

<!-- 1、定义 SQL -->
<mapper namespace="dao">
    <select id="selectAll" parameterType="user">
        select * from t_user 
        <if test="id != null">
            where id = #{id}
        </if>
    </select>
</mapper>
// 2、执行 SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1);  // 有 id

User user2 = new User(); 
sqlSession.select("dao.selectAll",user2);  // 无 id

认真察看,以上这个 SQL 语句,额定增加了一块 if 标签 作为条件判断,所以应用程序在编译阶段是无奈确定 SQL 语句最终主体构造的,只有在运行时依据应用程序是否传入 id 这个条件,来动静的拼接最终执行的 SQL 语句,因而属于动静 SQL。

另外,还有一种常见的状况,大家看看上面这个 SQL 语句算是动静 SQL 语句吗?

<!-- 1、定义 SQL -->
<mapper namespace="dao">
    <select id="selectAll" parameterType="user">
        select * from t_user where id = #{id} 
    </select>
</mapper>
// 2、执行 SQL
User user1 = new User();
user1.setId(1);
sqlSession.select("dao.selectAll",user1);  // 有 id

依据动静 SQL 的定义,大家是否能判断以上的语句块是否属于动静 SQL?

答案:不属于动静 SQL!

起因很简略,这个 SQL 在编译阶段就曾经明确主体构造了,尽管内部动静的传入一个 id,可能是 1,可能是 2,可能是 100,然而因为它的主体构造曾经确定,这个语句就是查问一个指定 id 的用户记录,它最终执行的 SQL 语句不会有任何动静的变动,所以顶多算是一个反对动静传参的动态 SQL。

至此,咱们对于动静 SQL 和动态 SQL 的区别曾经有了一个根底认知,然而有些好奇的敌人又会思考另一个问题:动静 SQL 是 Mybatis 独有的吗?

2、动静 SQL 的诞生记

咱们都晓得,SQL 是一种平凡的数据库语言 规范,在数据库管理系统纷争的时代,它的呈现对立标准了数据库操作语言,而此时,市面上的数据库管理软件百花齐放,我最早应用的 SQL Server 数据库,过后用的数据库管理工具是 SQL Server Management Studio,起初接触 Oracle 数据库,用了 PL/SQL Developer,再起初直至今日就简直都在用 MySQL 数据库(这个跟各种云厂商崛起无关),所以根本应用 Navicat 作为数据库管理工具,当然现在市面上还有许多许多,数据库管理工具嘛,只有能便捷高效的治理咱们的数据库,那就是好工具,duck 不用纠结抉择哪一款!

那这么多好工具,都提供什么性能呢?置信咱们平时接触最多的就是接管执行 SQL 语句的输出界面(也称为查问编辑器),这个输出界面简直反对所有 SQL 语法,例如咱们编写一条语句查问 id 等于 15 的用户数据记录:

select * from user where id = 15 ;

咱们来看一下这个查问后果:

很显然,在这个输出界面内输出的任何 SQL 语句,对于数据库管理工具来说,都是 动静 SQL!因为工具自身并不可能提前晓得用户会输出什么 SQL 语句,只有当用户执行之后,工具才接管到用户理论输出的 SQL 语句,能力最终确定 SQL 语句的主体构造,当然!即便咱们不通过可视化的数据库管理工具,也能够用数据库自身自带反对的命令行工具来执行 SQL 语句。但无论用户应用哪类工具,输出的语句都会被工具认为是 动静 SQL

这么一说,动静 SQL 原来不是 Mybatis 独有的个性!其实除了以上介绍的数据库管理工具以外,在纯 JDBC 时代,咱们就常常通过字符串来动静的拼接 SQL 语句,这也是在高级语言环境(例如 Java 语言编程环境)中晚期罕用的动静 SQL 构建形式!

// 内部条件 id
Integer id = Integer.valueOf(15);

// 动静拼接 SQL
StringBuilder sql = new StringBuilder();
sql.append("select  *");
sql.append("from user");

// 依据内部条件 id 动静拼接 SQL
if (null != id){sql.append("where id =" + id);
}

// 执行语句
connection.prepareStatement(sql);

只不过,这种构建动静 SQL 的形式,存在很大的平安问题和异样危险(咱们第 5 点会具体介绍),所以不倡议应用,起初 Mybatis 入世之后,在看待动静 SQL 这件事上,就分外上心,它默默赌咒,肯定要为应用 Mybatis 框架的用户提供一套棒棒的计划(标签)来灵便构建动静 SQL!

于是乎,Mybatis 借助 OGNL 的表达式的平凡设计,可算在动静 SQL 构建方面提供了各类功能强大的辅助标签,咱们简略列举一下有:if、choose、when、otherwise、trim、where、set、foreach、bind 等,我顺手翻了翻我电脑外头已经保留的学习笔记,咱们一起在第 3 节中温故知新,具体的讲一讲吧~

另外,须要纠正一点,就是咱们素日里在 Mybatis 框架中常说的动静 SQL,其实特指的也就是 Mybatis 框架中的这一套动静 SQL 标签 ,或者说是这一 个性,而并不是在说动静 SQL 自身。

3、动静 SQL 标签的 9 大标签

很好,可算进入咱们动静 SQL 标签的主题,依据后面的铺垫,其实咱们都能发现,很多时候动态 SQL 语句并不能满足咱们简单的业务场景需要,所以咱们须要有适当灵便的一套形式或者能力,来便捷高效的构建动静 SQL 语句,去匹配咱们动态变化的业务需要。举个栗子,在上面此类多条件的场景需要之下,动静 SQL 语句就显得尤为重要(先退场 if 标签)。

当然,很多敌人会说这类需要,不能用 SQL 来查,得用搜索引擎,的确如此。然而呢,在咱们的理论业务需要当中,还是存在很多没有引入搜索引擎零碎,或者有些基本无需引入搜索引擎的应用程序或性能,它们也会波及到多选项多条件或者多后果的业务需要,那此时也就的确须要应用动静 SQL 标签来灵便构建执行语句。

那么,Mybatis 目前都提供了哪些棒棒的动静 SQL 标签呢?咱们先引出一个类叫做 XMLScriptBuilder,大家先简略了解它是负责解析咱们的动静 SQL 标签的这么一个构建器,在第 4 点底层原理中咱们再具体介绍。

// XML 脚本标签构建器
public class XMLScriptBuilder{
    
    // 标签节点处理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();
    
    // 结构器
    public XMLScriptBuilder() {initNodeHandlerMap();
        //... 其它初始化不赘述也不重要
    }
    
    // 初始化
    private void initNodeHandlerMap() {nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
}

其实源码中很清晰得体现,一共有 9 大动静 SQL 标签!Mybatis 在初始化解析配置文件的时候,会实例化这么一个标签节点的结构器,那么它自身就会提前把所有 Mybatis 反对的动静 SQL 标签对象对应的处理器给进行一个实例化,而后放到一个 Map 池子外头,而这些处理器,都是该类 XMLScriptBuilder 的一个匿名外部类,而匿名外部类的性能也很简略,就是解析解决对应类型的标签节点,在后续应用程序应用动静标签的时候,Mybatis 随时到 Map 池子中匹配对应的标签节点处理器,而后进解析即可。上面咱们别离对这 9 大动静 SQL 标签进行介绍,排(gen)名(ju)不(wo)分(de)先(xi)后(hao):


Top1、if 标签

罕用度:★★★★★

实用性:★★★★☆

if 标签,相对算得上是一个平凡的标签,任何不反对流程管制(或语句管制)的应用程序,都是耍流氓,简直都不具备现实意义,理论的利用场景和流程必然存在条件的管制与流转,而 if 标签在 单条件分支判断 利用场景中就起到了舍我其谁的作用,语法很简略,如果满足,则执行,不满足,则疏忽 / 跳过。

  • if 标签:内嵌于 select / delete / update / insert 标签,如果满足 test 属性的条件,则执行代码块
  • test 属性:作为 if 标签的属性,用于条件判断,应用 OGNL 表达式。

举个例子:

<select id="findUser">
    select * from User where 1=1
    <if test="age != null">
        and age > #{age}
    </if>
    <if test="name != null">
        and name like concat(#{name},'%')
    </if>
</select>

很显著,if 标签元素罕用于蕴含 where 子句的条件拼接,它相当于 Java 中的 if 语句,和 test 属性搭配应用,通过判断参数值来决定是否应用某个查问条件,也可用于 Update 语句中判断是否更新某个字段,或用于 Insert 语句中判断是否插入某个字段的值。

每一个 if 标签在进行单条件判断时,须要把判断条件设置在 test 属性中,这是一个常见的利用场景,咱们罕用的用户查问零碎性能中,在前端个别提供很多可选的查问项,反对性别筛选、年龄区间筛查、姓名含糊匹配等,那么咱们程序中接管用户输出之后,Mybatis 的动静 SQL 节俭咱们很多工作,容许咱们在代码层面不进行参数逻辑解决和 SQL 拼接,而是把参数传入到 SQL 中进行条件判断动静解决,咱们只须要把精力集中在 XML 的保护上,既灵便也不便保护,可读性还强。

有些心细的敌人可能就发现一个问题,为什么 where 语句会增加一个 1=1 呢?其实咱们是为了不便拼接前面符合条件的 if 标签语句块,否则没有 1=1 的话咱们拼接的 SQL 就会变成 select * from user where and age > 0 , 显然这不是咱们冀望的后果,当然也不合乎 SQL 的语法,数据库也不可能执行胜利,所以咱们投机取巧增加了 1=1 这个语句,然而始终感觉多余且没必要,Mybatis 也思考到了,所以等会咱们讲 where 标签,它是如何完满解决这个问题的。

留神:if 标签作为单条件分支判断,只能管制与非此即彼的流程,例如以上的例子,如果年龄 age 和姓名 name 都不存在,那么零碎会把所有后果都查问进去,但有些时候,咱们心愿零碎更加灵便,能有更多的流程分支,例如像咱们 Java 当中的 if else 或 switch case default,不仅仅只有一个条件分支,所以接下来咱们介绍 choose 标签,它就能满足多分支判断的利用场景。


Top2、choose 标签、when 标签、otherwise 标签

罕用度:★★★★☆

实用性:★★★★☆

有些时候,咱们并不心愿条件管制是非此即彼的,而是心愿能提供多个条件并从中抉择一个,所以贴心的 Mybatis 提供了 choose 标签元素,相似咱们 Java 当中的 if else 或 switch case default,choose 标签必须搭配 when 标签和 otherwise 标签应用,验证条件仍然是应用 test 属性进行验证。

  • choose 标签:顶层的多分支标签,独自应用无意义
  • when 标签:内嵌于 choose 标签之中,当满足某个 when 条件时,执行对应的代码块,并终止跳出 choose 标签,choose 中必须至多存在一个 when 标签,否则无意义
  • otherwise 标签:内嵌于 choose 标签之中,当不满足所有 when 条件时,则执行 otherwise 代码块,choose 中 至少 存在一个 otherwise 标签,能够不存在该标签
  • test 属性:作为 when 与 otherwise 标签的属性,作为条件判断,应用 OGNL 表达式

根据上面的例子,当应用程序输出年龄 age 或者姓名 name 时,会执行对应的 when 标签内的代码块,如果 when 标签的年龄 age 和姓名 name 都不满足,则会拼接 otherwise 标签内的代码块。

<select id="findUser">
    select * from User where 1=1 
    <choose>
        <when test="age != null">
            and age > #{age}
        </when>
        <when test="name != null">
            and name like concat(#{name},'%')
        </when>
        <otherwise>
            and sex = '男'
        </otherwise>
    </choose>
</select>

很显著,choose 标签作为多分支条件判断,提供了更多灵便的流程管制,同时 otherwise 的呈现也为程序流程管制兜底,有时可能防止局部零碎危险、过滤局部条件、防止当程序没有匹配到条件时,把整个数据库资源全副查问或更新。

至于为何 choose 标签这么棒棒,而罕用度还是比 if 标签少了一颗星呢?起因也简略,因为 choose 标签的很多应用场景能够间接用 if 标签代替。另外据我统计,if 标签在理论业务利用当中,也要多于 choose 标签,大家也能够具体核查本人的应用程序中动静 SQL 标签的占比状况,统计分析一下。


Top3、foreach 标签

罕用度:★★★☆☆

实用性:★★★★☆

有些场景,可能须要查问 id 在 1 ~ 100 的用户记录

有些场景,可能须要批量插入 100 条用户记录

有些场景,可能须要更新 500 个用户的姓名

有些场景,可能须要你删除 10 条用户记录

请问大家

很多增删改查场景,操作对象都是汇合 / 列表

如果是你来设计反对 Mybatis 的这一类汇合 / 列表遍历场景,你会提供什么能力的标签来辅助构建你的 SQL 语句从而去满足此类业务场景呢?

额(⊙o⊙)…

那如果肯定要用 Mybatis 框架呢?

没错,的确 Mybatis 提供了 foreach 标签来解决这几类须要遍历汇合的场景,foreach 标签作为一个循环语句,他可能很好的反对数组、Map、或实现了 Iterable 接口(List、Set)等,尤其是在构建 in 条件语句的时候,咱们惯例的用法都是 id in (1,2,3,4,5 … 100),实践上咱们能够在程序代码中拼接字符串而后通过 ${ids} 形式来传值获取,然而这种形式不能避免 SQL 注入危险,同时也特地容易拼接谬误,所以咱们此时就须要应用 #{} + foreach 标签来配合应用,以满足咱们理论的业务需要。譬如咱们传入一个 List 列表查问 id 在 1 ~ 100 的用户记录:

<select id="findAll">
    select  * from user where ids in 
    <foreach collection="list"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

最终拼接残缺的语句就变成:


select  * from user where ids in (1,2,3,...,100);

当然你也能够这样编写:

<select id="findAll">
    select  * from user where 
    <foreach collection="list"
        item="item" index="index" 
        open="" separator=" or "close=" ">
            id = #{item}
    </foreach>
</select>

最终拼接残缺的语句就变成:


select  * from user where id =1 or id =2 or id =3  ... or id = 100;

在数据量大的状况下这个性能会比拟难堪,这里仅仅做一个用法的举例。所以通过下面的举栗,置信大家也根本能猜出 foreach 标签元素的根本用法:

  • foreach 标签:顶层的遍历标签,独自应用无意义
  • collection 属性:必填,Map 或者数组或者列表的属性名(不同类型的值获取上面会解说)
  • item 属性:变量名,值为遍历的每一个值(能够是对象或根底类型),如果是对象那么仍旧是 OGNL 表达式取值即可,例如 #{item.id}、#{user.name} 等
  • index 属性:索引的属性名,在遍历列表或数组时为以后索引值,当迭代的对象时 Map 类型时,该值为 Map 的键值(key)
  • open 属性:循环内容结尾拼接的字符串,能够是空字符串
  • close 属性:循环内容结尾拼接的字符串,能够是空字符串
  • separator 属性:每次循环的分隔符

第一,当传入的参数为 List 对象时,零碎会默认增加一个 key 为 ‘list’ 的值,把列表内容放到这个 key 为 list 的汇合当中,在 foreach 标签中能够间接通过 collection=”list” 获取到 List 对象,无论你传入时应用 kkk 或者 aaa , 都无所谓,零碎都会默认增加一个 key 为 list 的值,并且 item 指定遍历的对象值,index 指定遍历索引值。

// java 代码
List kkk = new ArrayList();
kkk.add(1);
kkk.add(2);
...
kkk.add(100);
sqlSession.selectList("findAll",kkk);
<!-- xml 配置 -->
<select id="findAll">
    select  * from user where ids in 
    <foreach collection="list"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

第二,当传入的参数为数组时,零碎会默认增加一个 key 为 ‘array’ 的值,把列表内容放到这个 key 为 array 的汇合当中,在 foreach 标签中能够间接通过 collection=”array” 获取到数组对象,无论你传入时应用 ids 或者 aaa , 都无所谓,零碎都会默认增加一个 key 为 array 的值,并且 item 指定遍历的对象值,index 指定遍历索引值。

// java 代码
String [] ids = new String[3];
ids[0] = "1";
ids[1] = "2";
ids[2] = "3";
sqlSession.selectList("findAll",ids);
<!-- xml 配置 -->
<select id="findAll">
    select  * from user where ids in 
    <foreach collection="array"
        item="item" index="index" 
        open="(" separator="," close=")">
            #{item}
    </foreach>
</select>

第三,当传入的参数为 Map 对象时 ,零碎并 不会 默认增加一个 key 值,须要手工传入,例如传入 key 值为 map2 的汇合对象,在 foreach 标签中能够间接通过 collection=”map2″ 获取到 Map 对象,并且 item 代表每次迭代的的 value 值,index 代表每次迭代的 key 值。其中 item 和 index 的值名词能够随便定义,例如 item = “value111″,index =”key111″。

// java 代码
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3);

Map map1 = new HashMap<>();
map1.put("map2",map2);
sqlSession.selectList("findAll",map1);

挺闹心,map1 套着 map2,能力在 foreach 的 collection 属性中获取到。

<!-- xml 配置 -->
<select id="findAll">
    select  * from user where
    <foreach collection="map2"
        item="value111" index="key111" 
        open="" separator=" or "close=" ">
        id = #{value111}
    </foreach>
</select>

可能你会感觉 Map 受到不偏心看待,为何 map 不能像 List 或者 Array 一样,在框架默认设置一个 ‘map’ 的 key 值呢?但其实不是不偏心,而是咱们在 Mybatis 框架中,所有传入的任何参数都会供上下文应用,于是参数会被对立放到一个内置参数池子外面,这个内置参数池子的数据结构是一个 map 汇合,而这个 map 汇合能够通过应用“_parameter”来获取,所有 key 都会存储在 _parameter 汇合中,因而:

  • 当你传入的参数是一个 list 类型时,那么这个参数池子须要有一个 key 值,以供上下文获取这个 list 类型的对象,所以默认设置了一个 ‘list’ 字符串作为 key 值,获取时通过应用 _parameter.list 来获取,个别应用 list 即可。
  • 同样的,当你传入的参数是一个 array 数组时,那么这个参数池子也会默认设置了一个 ‘array’ 字符串作为 key 值,以供上下文获取这个 array 数组的对象值,获取时通过应用 _parameter.array 来获取,个别应用 array 即可。
  • 然而!当你传入的参数是一个 map 汇合类型时,那么这个参数池就没必要为你增加默认 key 值了,因为 map 汇合类型自身就会有很多 key 值,例如你想获取 map 参数的某个 key 值,你能够间接应用 _parameter.name 或者 _parameter.age 即可,就没必要还用 _parameter.map.name 或者 _parameter.map.age,所以这就是 map 参数类型无需再构建一个 ‘map’ 字符串作为 key 的起因,对象类型也是如此,例如你传入一个 User 对象。

因而,如果是 Map 汇合,你能够这么应用:

// java 代码
Map map2 = new HashMap<>();
map2.put("k1",1);
map2.put("k2",2);
map2.put("k3",3); 
sqlSession.selectList("findAll",map2);

间接应用 collection=”_parameter”,你会发现神奇的 key 和 value 都能通过 _parameter 遍历在 index 与 item 之中。

<!-- xml 配置 -->
<select id="findAll">
    select  * from user where
    <foreach collection="_parameter"
         item="value111" index="key111"
         open="" separator=" or "close=" ">
        id = #{value111}
    </foreach>
</select>

延长:当传入参数为多个对象时,例如传入 User 和 Room 等,那么通过内置参数获取对象能够应用 _parameter.get(0).username,或者 _parameter.get(1).roomname。如果你传入的参数是一个简略数据类型,例如传入 int =1 或者 String = ‘ 你好 ’,那么都能够间接应用 _parameter 代替获取值即可,这就是很多人会在动静 SQL 中间接应用 # {_parameter} 来获取简略数据类型的值。

那到这里,咱们根本把 foreach 根本用法介绍实现,不过以上只是针对查问的应用场景,对于删除、更新、插入的用法,也是大同小异,咱们简略说一下,如果你心愿批量插入 100 条用户记录:

<insert id="insertUser" parameterType="java.util.List">
    insert into user(id,username) values
    <foreach collection="list" 
         item="user" index="index"
         separator="," close=";" >
        (#{user.id},#{user.username})
    </foreach>
</insert>

如果你心愿更新 500 个用户的姓名:

<update id="updateUser" parameterType="java.util.List">
    update user 
       set username = '潘潘' 
     where id in 
    <foreach collection="list"
        item="user" index="index" 
        separator="," open="(" close=")" >
        #{user.id}    
    </foreach>
</update>

如果你心愿你删除 10 条用户记录:

<delete id="deleteUser" parameterType="java.util.List">
    delete from user  
          where id in 
    <foreach collection="list"
         item="user" index="index" 
         separator="," open="(" close=")" >
        #{user.id}    
    </foreach>
</delete>

更多玩法,期待你本人去开掘!

留神:应用 foreach 标签时,须要对传入的 collection 参数(List/Map/Set 等)进行为空性判断,否则动静 SQL 会呈现语法异样,例如你的查问语句可能是 select * from user where ids in (),导致以上语法异样就是传入参数为空,解决方案能够用 if 标签或 choose 标签进行为空性判断解决,或者间接在 Java 代码中进行逻辑解决即可,例如判断为空则不执行 SQL。


Top4、where 标签、set 标签

罕用度:★★☆☆☆

实用性:★★★★☆

咱们把 where 标签和 set 标签搁置一起解说,一是这两个标签在理论利用开发中罕用度的确不分伯仲,二是这两个标签出自一家,都继承了 trim 标签,搁置一起不便咱们比对追根。(其中底层原理会在第 4 局部具体解说)

之前咱们介绍 if 标签的时候,置信大家都曾经看到,咱们在 where 子句前面拼接了 1=1 的条件语句块,目标是为了保障后续条件可能正确拼接,以前在程序代码中应用字符串拼接 SQL 条件语句经常如此应用,然而的确此种形式不够体面,也显得咱们不高级。

<select id="findUser">
    select * from User where 1=1
    <if test="age != null">
        and age > #{age}
    </if>
    <if test="name != null">
        and name like concat(#{name},'%')
    </if>
</select>

以上是咱们应用 1=1 的写法,那 where 标签诞生之后,是怎么奇妙解决后续的条件语句的呢?

<select id="findUser">
    select * from User 
    <where>
        <if test="age != null">
            and age > #{age}
        </if>
        <if test="name != null">
            and name like concat(#{name},'%')
        </if>
    </where>
</select>

咱们只需把 where 关键词以及 1=1 改为 < where > 标签即可,另外还有一个非凡的解决能力,就是 where 标签可能智能的去除(疏忽)首个满足条件语句的前缀,例如以上条件如果 age 和 name 都满足,那么 age 前缀 and 会被智能去除掉,无论你是应用 and 运算符或是 or 运算符,Mybatis 框架都会帮你智能解决。

用法特地简略,咱们用官术总结一下

  • where 标签:顶层的遍历标签,须要配合 if 标签应用,独自应用无意义,并且只会在子元素(如 if 标签)返回任何内容的状况下才插入 WHERE 子句。另外,若子句的结尾为“AND”或“OR”,where 标签也会将它替换去除。

理解了根本用法之后,咱们再看看刚刚咱们的例子中:

<select id="findUser">
    select * from User 
    <where>
        <if test="age != null">
            and age > #{age}
        </if>
        <if test="name != null">
            and name like concat(#{name},'%')
        </if>
    </where>
</select>

如果 age 传入有效值 10,满足 age != null 的条件之后,那么就会返回 where 标签并去除首个子句运算符 and,最终的 SQL 语句会变成:

select * from User where age > 10; 
-- and 奇妙的不见了

值得注意的是,where 标签 只会 智能的去除(疏忽)首个满足条件语句的前缀,所以就倡议咱们在应用 where 标签的时候,每个语句都最好写上 and 前缀或者 or 前缀,否则像以下写法就很有可能出小事:

<select id="findUser">
    select * from User 
    <where>
        <if test="age != null">
             age > #{age} 
             <!-- age 前缀没有运算符 -->
        </if>
        <if test="name != null">
             name like concat(#{name},'%')
             <!-- name 前缀也没有运算符 -->
        </if>
    </where>
</select>

当 age 传入 10,name 传入‘潘潘’时,最终的 SQL 语句是:

select * from User 
where 
age > 10 
name like concat('潘 %')
-- 所有条件都没有 and 或 or 运算符
-- 这让 age 和 name 显得很难堪~

因为 name 前缀没有写 and 或 or 连接符,而 where 标签又不会智能的去除(疏忽)非首个 满足条件语句的前缀,所以当 age 条件语句与 name 条件语句同时成立时,就会导致语法错误,这个须要审慎应用,分外留神!原则上每个条件子句都倡议在句首增加运算符 and 或 or , 首个条件语句可增加可不加。

另外还有一个值得注意的点,咱们应用 XML 形式配置 SQL 时,如果在 where 标签之后增加了正文,那么当有子元素满足条件时,除了 < !– –> 正文会被 where 疏忽解析以外,其它正文例如 // 或 /**/ 或 — 等都会被 where 当成首个子句元素解决,导致后续真正的首个 AND 子句元素或 OR 子句元素没能被胜利替换掉前缀,从而引起语法错误!

基于 where 标签元素的解说,有助于咱们疾速了解 set 标签元素,毕竟它俩是如此相像。咱们回顾一下以往咱们的更新 SQL 语句:

<update id="updateUser">
    update user 
       set age = #{age},
           username = #{username},
           password = #{password} 
     where id =#{id}
</update> 

以上语句是咱们日常用于更新指定 id 对象的 age 字段、username 字段以及 password 字段,然而很多时候,咱们可能只心愿更新对象的某些字段,而不是每次都更新对象的所有字段,这就使得咱们在语句构造的构建上显得苍白有力。于是有了 set 标签元素。

用法与 where 标签元素类似

  • set 标签:顶层的遍历标签,须要配合 if 标签应用,独自应用无意义,并且只会在子元素(如 if 标签)返回任何内容的状况下才插入 set 子句。另外,若子句的 结尾或结尾 都存在逗号“,”则 set 标签都会将它替换去除。

依据此用法咱们能够把以上的例子改为:

<update id="updateUser">
    update user 
        <set>
           <if test="age !=null">
               age = #{age},
           </if>
           <if test="username !=null">
                  username = #{username},
           </if> 
           <if test="password !=null">
                  password = #{password},
           </if>
        </set>    
     where id =#{id}
</update> 

很简略易懂,set 标签会智能拼接更新字段,以上例子如果传入 age =10 和 username = ‘ 潘潘 ’,则有两个字段满足更新条件,于是 set 标签会智能拼接 ” age = 10 ,” 和 “username = ‘ 潘潘 ’ ,”。其中因为后一个 username 属于最初一个子句,所以开端逗号会被智能去除,最终的 SQL 语句是:

update user set age = 10,username =  '潘潘' 

另外须要留神,set 标签下须要保障至多有一个条件满足,否则仍然会产生语法错误,例如在无子句条件满足的场景下,最终的 SQL 语句会是这样:

update user ;  (oh~ no!)

既不会增加 set 标签,也没有子句更新字段,于是语法呈现了谬误,所以相似这类状况,个别须要在应用程序中进行逻辑解决,判断是否存在至多一个参数,否则不执行更新 SQL。所以原则上要求 set 标签下至多存在一个条件满足,同时每个条件子句都倡议在句末增加逗号 , 最初一个条件语句可加可不加。或者 每个条件子句都在句首增加逗号 , 第一个条件语句可加可不加,例如:

<update id="updateUser">
    update user 
        <set>
           <if test="age !=null">
               ,age = #{age}
           </if>
           <if test="username !=null">
                  ,username = #{username}
           </if> 
           <if test="password !=null">
                  ,password = #{password}
           </if>
        </set>    
     where id =#{id}
</update> 

与 where 标签雷同,咱们应用 XML 形式配置 SQL 时,如果在 set 标签子句开端增加了正文,那么当有子元素满足条件时,除了 < !– –> 正文会被 set 疏忽解析以外,其它正文例如 // 或 /**/ 或 — 等都会被 set 标签当成开端子句元素解决,导致后续真正的开端子句元素的逗号没能被胜利替换掉后缀,从而引起语法错误!

到此,咱们的 where 标签元素与 set 标签就根本介绍实现,它俩的确极为类似,区别仅在于:

  • where 标签插入前缀 where
  • set 标签插入前缀 set
  • where 标签仅智能替换前缀 AND 或 OR
  • set 标签能够只能替换前缀逗号,或后缀逗号,

而这两者的前后缀去除策略,都源自于 trim 标签的设计,咱们一起看看到底 trim 标签是有多灵便!


Top5、trim 标签

罕用度:★☆☆☆☆

实用性:★☆☆☆☆

下面咱们介绍了 where 标签与 set 标签,它俩的共同点无非就是前置关键词 where 或 set 的插入,以及前后缀符号(例如 AND | OR |,)的智能去除。基于 where 标签和 set 标签自身都继承了 trim 标签,所以 trim 标签的大抵实现咱们也能猜出个一二三。

其实 where 标签和 set 标签都只是 trim 标签的某种实现计划,trim 标签底层是通过 TrimSqlNode 类来实现的,它有几个要害属性:

  • prefix:前缀,当 trim 元素内存在内容时,会给内容插入指定前缀
  • suffix:后缀,当 trim 元素内存在内容时,会给内容插入指定后缀
  • prefixesToOverride:前缀去除,反对多个,当 trim 元素内存在内容时,会把内容中匹配的前缀字符串去除。
  • suffixesToOverride:后缀去除,反对多个,当 trim 元素内存在内容时,会把内容中匹配的后缀字符串去除。

所以 where 标签如果通过 trim 标签实现的话能够这么编写:(

<!--
  留神在应用 trim 标签实现 where 标签能力时
  必须在 AND 和 OR 之后增加空格
  防止匹配到 android、order 等单词 
-->
<trim prefix="WHERE" prefixOverrides="AND | OR" >
    ...
</trim>

而 set 标签如果通过 trim 标签实现的话能够这么编写:

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

或者

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

所以可见 trim 是足够灵便的,不过因为 where 标签和 set 标签这两种 trim 标签变种计划曾经足以满足咱们理论开发需要,所以间接应用 trim 标签的场景实际上不太很多(其实是我本人应用的不多,根本没用过)。

留神,set 标签之所以可能反对去除前缀逗号或者后缀逗号,是因为其在结构 trim 标签的时候进行了前缀后缀的去除设置,而 where 标签在结构 trim 标签的时候就仅仅设置了前缀去除。

set 标签元素之结构时:

// Set 标签
public class SetSqlNode extends TrimSqlNode {private static final List<String> COMMA = Collections.singletonList(",");

  // 显著应用了前缀后缀去除,留神前后缀参数都传入了 COMMA 
  public SetSqlNode(Configuration configuration,SqlNode contents) {super(configuration, contents, "SET", COMMA, null, COMMA);
  }

}

where 标签元素之结构时:

// Where 标签
public class WhereSqlNode extends TrimSqlNode {

  // 其实蕴含了很多种场景
  private static List<String> prefixList = Arrays.asList("AND","OR","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t");

  // 显著只应用了前缀去除,留神前缀传入 prefixList,后缀传入 null 
  public WhereSqlNode(Configuration configuration, SqlNode contents) {super(configuration, contents, "WHERE", prefixList, null, null);
  }

}

Top6、bind 标签

罕用度:☆☆☆☆☆

实用性:★☆☆☆☆

简略来说,这个标签就是能够创立一个变量,并绑定到上下文,即供上下文应用,就是这样,我把官网的例子间接拷贝过去:

<select id="selecUser">
  <bind name="myName" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{myName}
</select>

大家应该大抵能晓得以上例子的效用,其实就是辅助构建含糊查问的语句拼接,那有人就好奇了,为啥不间接拼接语句就行了,为什么还要搞出一个变量,绕一圈呢?

我先问一个问题:平时你应用 mysql 都是如何拼接含糊查问 like 语句的?

select * from user where name like concat('%',#{name},'%')

的确如此,但如果有一天领导跟你说数据库换成 oracle 了,怎么办?下面的语句还能用吗?显著用不了,不能这么写,因为 oracle 尽管也有 concat 函数,然而只反对连贯两个字符串,例如你最多这么写:

select * from user where name like concat('%',#{name})

然而少了左边的井号符号,所以达不到你预期的成果,于是你改成这样:

select * from user where name like '%'||#{name}||'%'

的确能够了,然而过几天领导又跟你说,数据库换回 mysql 了?额… 那不好意思,你又得把相干应用到含糊查问的中央改回来。

select * from user where name like concat('%',#{name},'%')

很显然,数据库只有产生变更你的 sql 语句就得跟着改,特地麻烦,所以才有了一开始咱们介绍 bind 标签官网的这个例子,无论应用哪种数据库,这个含糊查问的 Like 语法都是反对的:

<select id="selecUser">
  <bind name="myName" value="'%' + _parameter.getName() + '%'" />
  SELECT * FROM user
  WHERE name LIKE #{myName}
</select>

这个 bind 的用法,实打实解决了数据库从新选型后导致的一些问题,当然在理论工作中产生的概率不会太大,所以 bind 的应用我集体的确也应用的不多,可能还有其它一些利用场景,心愿有人能发现之后来跟咱们分享一下,总之我勉强给了一颗星(尽管没太多理论用途,但毕竟要给点体面)。


拓展:sql 标签 + include 标签

罕用度:★★★☆☆

实用性:★★★☆☆

sql 标签与 include 标签组合应用,用于 SQL 语句的复用,日常高频或专用应用的语句块能够抽取进去进行复用,其实咱们应该不生疏,晚期咱们学习 JSP 的时候,就有一个 include 标记能够引入一些专用可复用的页面文件,例如页面头部或尾部页面代码元素,这种复用的设计很常见。

严格意义上 sql、include 不算在动静 SQL 标签成员之内,只因它的确是宝藏般的存在,所以我要简略说说,sql 标签用于定义一段可重用的 SQL 语句片段,以便在其它语句中应用,而 include 标签则通过属性 refid 来援用对应 id 匹配的 sql 标签语句片段。

简略的复用代码块能够是:

<!-- 可复用的字段语句块 -->
<sql id="userColumns">
    id,username,password 
</sql>

查问或插入时简略复用:

<!-- 查问时简略复用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns"></include> 
  from user 
</select>

<!-- 插入时简略复用 -->
<insert id="insertUser" resultType="map">
  insert into user(<include refid="userColumns"></include>)values(#{id},#{username},#{password} 
  )  
</insert>

当然,复用语句还反对属性传递,例如:

<!-- 可复用的字段语句块 -->
<sql id="userColumns">
    ${pojo}.id,${pojo}.username 
</sql>

这个 SQL 片段能够在其它语句中应用:

<!-- 查问时复用 -->
<select id="selectUsers" resultType="map">
  select
    <include refid="userColumns">
        <property name="pojo" value="u1"/>
    </include>,
    <include refid="userColumns">
        <property name="pojo" value="u2"/>
    </include>
  from user u1 cross join user u2
</select>

也能够在 include 元素的 refid 属性或多层外部语句中应用属性值,属性能够穿透传递,例如:

<!-- 简略语句块 -->
<sql id="sql1">
  ${prefix}_user
</sql>

<!-- 嵌套语句块 -->
<sql id="sql2">
  from
    <include refid="${include_target}"/>
</sql>

<!-- 查问时援用嵌套语句块 -->
<select id="select" resultType="map">
  select
    id, username
  <include refid="sql2">
    <property name="prefix" value="t"/>
    <property name="include_target" value="sql1"/>
  </include>
</select>

至此,对于 9 大动静 SQL 标签的根本用法咱们已介绍结束,另外咱们还有一些疑难:Mybatis 底层是如何解析这些动静 SQL 标签的呢?最终又是怎么构建残缺可执行的 SQL 语句的呢?带着这些疑难,咱们在第 4 节中详细分析。

4、动静 SQL 的底层原理

想理解 Mybatis 到底是如何解析与构建动静 SQL?首先举荐的当然是读源码,而读源码,是一个技术钻研问题,为了借鉴学习,为了工作储备,为了解决问题,为了让本人在编程的路线上跑得明确一些 … 而心愿通过读源码,去理解底层实现原理,切记不能脱离了整体去读部分,否则你理解到的必然局限且全面,从而轻忽了真核上的设计。如同咱们读史或者观宇宙一样,最好的方法都是从整体到部分,一直放大,前后延展,会很难受通透。所以我筹备从 Mybatis 框架的外围主线下来逐渐放大分析。

通过后面几篇文章的介绍(倡议浏览 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你晓得几个?》),其实咱们晓得了 Mybatis 框架的外围局部在于构件的构建过程,从而撑持了内部应用程序的应用,从应用程序端创立配置并调用 API 开始,到框架端加载配置并初始化构件,再创立会话并接管申请,而后解决申请,最终返回处理结果等。

咱们的动静 SQL 解析局部就产生在 SQL 语句对象 MappedStatement 构建时(上左高亮橘色 局部,留神察看其中 SQL 语句对象与 SqlSource、BoundSql 的关系,在动静 SQL 解析流程特地要害)。咱们再拉近一点,能够看到无论是应用 XML 配置 SQL 语句或是应用注解形式配置 SQL 语句,框架最终都会把解析实现的 SQL 语句对象寄存到 MappedStatement 语句汇合池子。

而以上 虚线高亮 局部,即是 XML 配置形式解析过程与注解配置形式解析过程中波及到动静 SQL 标签解析的流程,咱们别离解说:

  • 第一,XML 形式配置 SQL 语句,框架如何解析?

以上为 XML 配置形式的 SQL 语句解析过程,无论是独自应用 Mybatis 框架还是集成 Spring 与 Mybatis 框架,程序启动入口都会首先从 SqlSessionFactoryBuilder.build() 开始构建,顺次通过 XMLConfigBuilder 构建全局配置 Configuration 对象、通过 XMLMapperBuilder 构建每一个 Mapper 映射器、通过 XMLStatementBuilder 构建映射器中的每一个 SQL 语句对象(select/insert/update/delete)。而就在解析构建每一个 SQL 语句对象时,波及到一个要害的办法 parseStatementNode(),即上图 橘红色高亮 局部,此办法外部就呈现了一个解决动静 SQL 的外围节点。

// XML 配置语句构建器
public class XMLStatementBuilder {
    
    // 理论解析每一个 SQL 语句
    // 例如 select|insert|update|delete
    public void parseStatementNode() {// [疏忽]参数构建...
        // [疏忽]缓存构建..
        // [疏忽]后果集构建等等.. 
        
        //【重点】此处即是解决动静 SQL 的外围!!!String lang = context.getStringAttribute("lang");
        LanguageDriver langDriver = getLanguageDriver(lang);
        SqlSource sqlSource = langDriver.createSqlSource(..);
        
        // [疏忽]最初把解析实现的语句对象增加进语句汇合池
        builderAssistant.addMappedStatement(语句对象)
    }
}

大家先重点关注一下这段代码,其中【重点】局部的 LanguageDriver 与 SqlSource 会是咱们接下来解说动静 SQL 语句解析的外围类,咱们不焦急分析,咱们先把注解形式流程也梳理比照一下。

  • 第二,注解形式配置 SQL 语句,框架如何解析?

大家会发现注解配置形式的 SQL 语句解析过程,与 XML 形式极为相像,惟一不同点就在于解析注解 SQL 语句时,应用了 MapperAnnotationBuilder 构建器,其中对于每一个语句对象 (@Select,@Insert,@Update,@Delete 等) 的解析,又都会通过一个要害解析办法 parseStatement(),即上图 橘红色高亮 局部,此办法外部同样的呈现了一个解决动静 SQL 的外围节点。

// 注解配置语句构建器
public class MapperAnnotationBuilder {
    
    // 理论解析每一个 SQL 语句
    // 例如 @Select,@Insert,@Update,@Delete
    void parseStatement(Method method) {// [疏忽]参数构建...
        // [疏忽]缓存构建..
        // [疏忽]后果集构建等等.. 
        
        //【重点】此处即是解决动静 SQL 的外围!!!final LanguageDriver languageDriver = getLanguageDriver(method);  
        final SqlSource sqlSource = buildSqlSource(languageDriver,...);
        
        // [疏忽]最初把解析实现的语句对象增加进语句汇合池
        builderAssistant.addMappedStatement(语句对象)

    }    
}

由此可见,不论是通过 XML 配置语句还是注解形式配置语句,构建流程都是 大致相同,并且仍然呈现了咱们在 XML 配置形式中波及到的语言驱动 LanguageDriver 与语句源 SqlSource,那这两个类 / 接口到底为何物,为何能让 SQL 语句解析者都如此绕不开?

这所有,得从你编写的 SQL 开始讲起 …

咱们晓得,无论 XML 还是注解,最终你的所有 SQL 语句对象都会被齐齐整整的解析完搁置在 SQL 语句对象汇合池中,以供执行器 Executor 具体执行增删改查 (CRUD) 时应用。而咱们晓得每一个 SQL 语句对象的属性,特地简单繁多,例如超时设置、缓存、语句类型、后果集映射关系等等。

// SQL 语句对象
public final class MappedStatement {

  private String resource;
  private Configuration configuration;
  private String id;
  private Integer fetchSize;
  private Integer timeout;
  private StatementType statementType;
  private ResultSetType resultSetType;
    
  // SQL 源
  private SqlSource sqlSource;
  private Cache cache;
  private ParameterMap parameterMap;
  private List<ResultMap> resultMaps;
  private boolean flushCacheRequired;
  private boolean useCache;
  private boolean resultOrdered;
  private SqlCommandType sqlCommandType;
  private KeyGenerator keyGenerator;
  private String[] keyProperties;
  private String[] keyColumns;
  private boolean hasNestedResultMaps;
  private String databaseId;
  private Log statementLog;
  private LanguageDriver lang;
  private String[] resultSets;}

而其中有一个特地的属性就是咱们的语句源 SqlSource,性能纯正也恰如其名 SQL 源。它是一个接口,它会联合用户传递的参数对象 parameterObject 与动静 SQL,生成 SQL 语句,并最终封装成 BoundSql 对象。SqlSource 接口有 5 个实现类,别离是:StaticSqlSource、DynamicSqlSource、RawSqlSource、ProviderSqlSource、VelocitySqlSource(而 velocitySqlSource 目前只是一个测试用例,还没有用作理论的 Sql 源实现)。

  • StaticSqlSource:动态 SQL 源实现类,所有的 SQL 源最终都会构建成 StaticSqlSource 实例,该实现类会生成最终可执行的 SQL 语句供 statement 或 prepareStatement 应用。
  • RawSqlSource:原生 SQL 源实现类,解析构建含有‘#{}’占位符的 SQL 语句或原生 SQL 语句,解析完最终会构建 StaticSqlSource 实例。
  • DynamicSqlSource:动静 SQL 源实现类,解析构建含有‘${}’替换符的 SQL 语句或含有动静 SQL 的语句(例如 If/Where/Foreach 等),解析完最终会构建 StaticSqlSource 实例。
  • ProviderSqlSource:注解形式的 SQL 源实现类,会依据 SQL 语句的内容分发给 RawSqlSource 或 DynamicSqlSource,当然最终也会构建 StaticSqlSource 实例。
  • VelocitySqlSource:模板 SQL 源实现类,目前(V3.5.6)官网申明这只是一个测试用例,还没有用作真正的模板 Sql 源实现类。

SqlSource 实例在配置类 Configuration 解析阶段就被创立,Mybatis 框架会根据 3 个维度的信息来抉择构建哪种数据源实例:(纯属我集体了解的归类梳理~)

  • 第一个维度:客户端的 SQL 配置形式:XML 形式或者注解形式。
  • 第二个维度:SQL 语句中是否应用动静 SQL(if/where/foreach 等)。
  • 第三个维度:SQL 语句中是否含有替换符‘${}’或占位符‘#{}’。

SqlSource 接口只有一个办法 getBoundSql,就是创立 BoundSql 对象。

public interface SqlSource {BoundSql getBoundSql(Object parameterObject);

}

通过 SQL 源就可能获取 BoundSql 对象,从而获取最终送往数据库(通过 JDBC)中执行的 SQL 字符串。

JDBC 中执行的 SQL 字符串,的确就在 BoundSql 对象中。BoundSql 对象存储了动静(或动态)生成的 SQL 语句以及相应的参数信息,它是在执行器具体执行 CURD 时通过理论的 SqlSource 实例所构建的。

public class BoundSql { 

  // 该字段中记录了 SQL 语句,该 SQL 语句中可能含有 "?" 占位符
  private final String sql;
    
  //SQL 中的参数属性汇合
  private final List<ParameterMapping> parameterMappings;
    
  // 客户端执行 SQL 时传入的理论参数值
  private final Object parameterObject;
    
  // 复制 DynamicContext.bindings 汇合中的内容
  private final Map<String, Object> additionalParameters;
    
  // 通过 additionalParameters 构建元参数对象
  private final MetaObject metaParameters;
    
}

在执行器 Executor 实例(例如 BaseExecutor)执行增删改查时,会通过 SqlSource 构建 BoundSql 实例,而后再通过 BoundSql 实例获取最终输送至数据库执行的 SQL 语句,零碎可依据 SQL 语句构建 Statement 或者 PrepareStatement,从而送往数据库执行,例如语句处理器 StatementHandler 的执行过程。

墙裂举荐浏览之前第六文之 Mybatis 最硬核的 API 你晓得几个?这些执行流程都有细讲。

到此咱们介绍完 SQL 源 SqlSource 与 BoundSql 的关系,留神 SqlSource 与 BoundSql 不是同个阶段产生的,而是别离在程序启动阶段与运行时。

  • 程序启动初始构建时,框架会依据 SQL 语句类型构建对应的 SqlSource 源实例(动态 / 动静).
  • 程序理论运行时,框架会依据传入参数动静的构建 BoundSql 对象,输送最终 SQL 到数据库执行。

在下面咱们晓得了 SQL 源是语句对象 BoundSql 的属性,同时还坐拥 5 大实现类,那到底是谁创立了 SQL 源呢?其实就是咱们接下来筹备介绍的语言驱动 LanguageDriver!

public interface LanguageDriver {SqlSource createSqlSource(...);
}

语言驱动接口 LanguageDriver 也是极简洁,外部定义了构建 SQL 源的办法,LanguageDriver 接口有 2 个实现类,别离是:XMLLanguageDriver、RawLanguageDriver。简略介绍一下:

  • XMLLanguageDriver:是框架默认的语言驱动,可能依据下面咱们解说的 SQL 源的 3 个维度创立对应匹配的 SQL 源(DynamicSqlSource、RawSqlSource 等)。上面这段代码是 Mybatis 在拆卸全局配置时的一些跟语言驱动相干的动作,我摘抄进去,别离有:内置了两种语言驱动并设置了别名不便援用、注册了两种语言驱动至语言注册工厂、把 XML 语言驱动设置为默认语言驱动。
// 全局配置的构造方法
public Configuration() {
    // 内置 / 注册了很多有意思的【别名】// ...
    
    // 其中就内置了上述的两种语言驱动【别名】typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
    
    // 注册了 XML【语言驱动】--> 并设置成默认!languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    
    // 注册了原生【语言驱动】languageRegistry.register(RawLanguageDriver.class);
}
  • RawLanguageDriver:看名字得悉是原生语言驱动,事实也如此,它只能创立原生 SQL 源(RawSqlSource),另外它还继承了 XMLLanguageDriver。
/**
 * As of 3.2.4 the default XML language is able to identify static statements
 * and create a {@link RawSqlSource}. So there is no need to use RAW unless you
 * want to make sure that there is not any dynamic tag for any reason.
 *
 * @since 3.2.0
 * @author Eduardo Macarron
 */
public class RawLanguageDriver extends XMLLanguageDriver {}

正文的大抵意思:自 Mybatis 3.2.4 之后的版本,XML 语言驱动就反对解析动态语句(动静语句当然也反对)并创立对应的 SQL 源(例如动态语句是原生 SQL 源),所以除非你非常确定你的 SQL 语句中没有蕴含任何一款动静标签,否则就不要应用 RawLanguageDriver!否则会报错!!!先看个别名援用的例子:

<select id="findAll"  resultType="map" lang="RAW" >
     select * from user
</select>

<!-- 别名或全限定类名都容许 -->

<select id="findAll"  resultType="map" lang="org.apache.ibatis.scripting.xmltags.XMLLanguageDriver">
     select * from user
</select>

框架容许咱们通过 lang 属性手工指定语言驱动,不指定则零碎默认是 lang = “XML”,XML 代表 XMLLanguageDriver,当然 lang 属性能够是咱们内置的别名也能够是咱们的语言驱动全限定名,不过值得注意的是,当语句中含有动静 SQL 标签时,就只能抉择应用 lang=”XML”,否则程序在初始化构件时就会报错。

## Cause: org.apache.ibatis.builder.BuilderException: 
## Dynamic content is not allowed when using RAW language
## 动静语句内容不被原生语言驱动反对!

这段谬误提醒其实是产生在 RawLanguageDriver 查看动静 SQL 源时:

public class RawLanguageDriver extends XMLLanguageDriver { 

  // RAW 不能蕴含动静内容
  private void checkIsNotDynamic(SqlSource source) {if (!RawSqlSource.class.equals(source.getClass())) {
      throw new BuilderException("Dynamic content is not allowed when using RAW language");
    }
  } 
}

至此,根本逻辑咱们曾经梳理分明:程序启动初始阶段,语言驱动创立 SQL 源,而运行时,SQL 源动静解析构建出 BoundSql。

那么除了零碎默认的两种语言驱动,还有其它吗?

答案是:有,例如 Mybatis 框架中目前应用了一个名为 VelocityLanguageDriver 的语言驱动。置信大家都学习过 JSP 模板引擎,同时还有很多人学习过其它一些(页面)模板引擎,例如 freemark 和 velocity,不同模板引擎有本人的一套模板语言语法,而其中 Mybatis 就尝试应用了 Velocity 模板引擎作为语言驱动,目前尽管 Mybatis 只是在测试用例中应用到,然而它通知了咱们,框架容许自定义语言驱动,所以不只是 XML、RAW 两种语言驱动中应用的 OGNL 语法,也能够是 Velocity(语法),或者你本人所能定义的一套模板语言(同时你得定义一套语法)。例如以下就是 Mybatis 框架中应用到的 Velocity 语言驱动和对应的 SQL 源,它们应用 Velocity 语法 / 形式解析构建 BoundSql 对象。

/**
 * Just a test case. Not a real Velocity implementation.
 * 只是一个测试示例,还不是一个真正的 Velocity 形式实现
 */
public class VelocityLanguageDriver implements LanguageDriver {public SqlSource createSqlSource() {...}
}
public class VelocitySqlSource implements SqlSource {public BoundSql getBoundSql() {...}
}

好,语言驱动的基本概念大抵如此。咱们回过头再具体看看动静 SQL 源 SqlSource,作为语句对象 MappedStatement 的属性,在 程序初始构建阶段,语言驱动是怎么创立它的呢?无妨咱们先看看罕用的动静 SQL 源对象是怎么被创立的吧!

通过以上的程序初始构建阶段,咱们能够发现,最终语言驱动通过调用 XMLScriptBuilder 对象来创立 SQL 源。

// XML 语言驱动
public class XMLLanguageDriver implements LanguageDriver {  
  
    // 通过调用 XMLScriptBuilder 对象来创立 SQL 源
    @Override
      public SqlSource createSqlSource() {
        // 实例
        XMLScriptBuilder builder = new XMLScriptBuilder();
        // 解析
        return builder.parseScriptNode();}
}

而在后面咱们就曾经介绍,XMLScriptBuilder 实例初始结构时,会初始构建所有动静标签处理器:

// XML 脚本标签构建器
public class XMLScriptBuilder{
    // 标签节点处理器池
    private final Map<String, NodeHandler> nodeHandlerMap = new HashMap<>();

    // 结构器
    public XMLScriptBuilder() {initNodeHandlerMap();
        //... 其它初始化不赘述也不重要
    }

    // 动静标签处理器
    private void initNodeHandlerMap() {nodeHandlerMap.put("trim", new TrimHandler());
        nodeHandlerMap.put("where", new WhereHandler());
        nodeHandlerMap.put("set", new SetHandler());
        nodeHandlerMap.put("foreach", new ForEachHandler());
        nodeHandlerMap.put("if", new IfHandler());
        nodeHandlerMap.put("choose", new ChooseHandler());
        nodeHandlerMap.put("when", new IfHandler());
        nodeHandlerMap.put("otherwise", new OtherwiseHandler());
        nodeHandlerMap.put("bind", new BindHandler());
    }
}

继 XMLScriptBuilder 初始化流程之后,解析创立 SQL 源流程再分为两步:

1、解析动静标签,通过判断每一块动静标签的类型,应用对应的标签处理器进行解析属性和语句解决,并最终搁置到混合 SQL 节点池中(MixedSqlNode),以供程序运行时构建 BoundSql 时应用。

2、new SQL 源,依据 SQL 是否有动静标签或通配符占位符来确认产生对象的动态或动静 SQL 源。

public SqlSource parseScriptNode() {
    
    // 1、解析动静标签,并放到混合 SQL 节点池中
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    
    // 2、依据语句类型,new 进去最终的 SQL 源
    SqlSource sqlSource;
    if (isDynamic) {sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
}

原来解析动静标签的工作交给了 parseDynamicTags() 办法,并且每一个语句对象的动静 SQL 标签最终都会被放到一个混合 SQL 节点池中。

// 混合 SQL 节点池
public class MixedSqlNode implements SqlNode {
    
    // 所有动静 SQL 标签:IF、WHERE、SET 等
    private final List<SqlNode> contents;
}

咱们先看一下 SqlNode 接口的实现类,根本涵盖了咱们所有动静 SQL 标签处理器所须要应用到的节点实例。而其中混合 SQL 节点 MixedSqlNode 作用仅是为了不便获取每一个语句的所有动静标签节点,于是应势而生。

晓得动静 SQL 标签节点处理器及以上的节点实现类之后,其实就能很容易了解,达到程序运行时,执行器会调用 SQL 源来帮助构建 BoundSql 对象,而 SQL 源的外围工作,就是依据每一小段标签类型,匹配到对应的节点实现类以解析拼接每一小段 SQL 语句。

程序运行时,动静 SQL 源获取 BoundSql 对象:

// 动静 SQL 源
public class DynamicSqlSource implements SqlSource { 
   
    // 这里的 rootSqlNode 属性就是 MixedSqlNode 
    private final SqlNode rootSqlNode;
  
    @Override
    public BoundSql getBoundSql(Object parameterObject) {
 
        // 动静 SQL 外围解析流程  
        rootSqlNode.apply(...);  
        
        return boundSql;

    } 
}

很显著,通过调用 MixedSqlNode 的 apply () 办法,循环遍历每一个具体的标签节点。

public class MixedSqlNode implements SqlNode {
    
      // 所有动静 SQL 标签:IF、WHERE、SET 等
      private final List<SqlNode> contents; 

      @Override
      public boolean apply(...) {

        // 循环遍历,把每一个节点的解析分派到具体的节点实现之上
        // 例如 <if> 节点的解析交给 IfSqlNode
        // 例如 纯文本节点的解析交给 StaticTextSqlNode
        contents.forEach(node -> node.apply(...));
        return true;
      }
}

咱们抉择一两个标签节点的解析过程进行阐明,其它标签节点实现类的解决也根本雷同。首先咱们看一下 IF 标签节点的解决:

// IF 标签节点
public class IfSqlNode implements SqlNode { 
    
      private final ExpressionEvaluator evaluator;
    
      // 实现逻辑
      @Override
      public boolean apply(DynamicContext context) {
          
        // evaluator 是一个基于 OGNL 语法的解析校验类
        if (evaluator.evaluateBoolean(test, context.getBindings())) {contents.apply(context);
          return true;
        }
        return false;
      } 
}

IF 标签节点的解析过程非常简单,通过解析校验类 ExpressionEvaluator 来对 IF 标签的 test 属性内的表达式进行解析校验,满足则拼接,不满足则跳过。咱们再看看 Trim 标签的节点解析过程,set 标签与 where 标签的底层解决都基于此:

public class TrimSqlNode implements SqlNode { 
    
    // 外围解决办法
    public void applyAll() {
        
        // 前缀智能补充与去除
        applyPrefix(..); 
        
        // 前缀智能补充与去除
        applySuffix(..); 
    } 
}

再来看一个纯文本标签节点实现类的解析解决流程:

// 纯文本标签节点实现类
public class StaticTextSqlNode implements SqlNode {
  
    private final String text;

    public StaticTextSqlNode(String text) {this.text = text;}
    
    // 节点解决,仅仅就是纯正的语句拼接
    @Override
    public boolean apply(DynamicContext context) {context.appendSql(text);
        return true;
      }
}

到这里,动静 SQL 的底层解析过程咱们根本解说完,简短了些,但流程上大抵算残缺,有脱漏的,咱们回头再补充。

总结

人不知; 鬼不觉中,我又是这么巨篇幅的解说分析,的确不太适宜碎片化工夫浏览,不过话说回来,毕竟此文属于 Mybatis 全解系列,作为学研者还是倡议深谙其中,对往后泛滥框架技术的学习必有帮忙。本文中咱们很多动静 SQL 的介绍根本都应用 XML 配置形式,当然注解形式配置动静 SQL 也是反对的,动静 SQL 的语法书写同 XML 形式,然而须要在字符串前后增加 script 标签申明该语句为动静 SQL,例如:

public class UserDao {
   
    /**
     * 更新用户
     */
    @Select(
        "<script>"+
        "UPDATE user"+
        "<trim prefix=\"SET\"prefixOverrides=\",\">"+
        "<if test=\"username != null and username != ''\"> "+"           , username = #{username} "+"       </if> "+"   </trim> "+"   where id = ${id}""</script>"
    )
    void updateUser(User user);
    
}

此种动静 SQL 写法可读性较差,并且保护起来也挺硌手,所以我集体是青眼 xml 形式配置语句,始终谋求解耦,小道也至简。当然,也有很多团队和我的项目都在应用注解形式开发,这些没有相对,还是得联合本人的理论我的项目状况与团队等去做取舍。

本篇完,本系列下一篇咱们讲《Mybatis 系列全解(九):Mybatis 的简单映射》。

文章继续更新,微信搜寻「潘潘和他的敌人们 」第一工夫浏览,随时有惊喜。本文会在 GitHub https://github.com/JavaWorld 收录,对于热腾腾的技术、框架、面经、解决方案、摸鱼技巧、教程、视频、漫画等等等等,咱们都会以最美的姿态第一工夫送达,欢送 Star ~ 咱们将来 不止文章 !想进读者群的敌人欢送撩我集体号:panshenlian,备注「 加群」咱们群里畅聊,BIU ~

正文完
 0