封面:洛小汐

作者:潘潘

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、执行SQLsqlSession.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、执行SQLUser user1 = new User();user1.setId(1);sqlSession.select("dao.selectAll",user1);  // 有 idUser 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、执行SQLUser 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 构建形式!

// 内部条件idInteger id = Integer.valueOf(15);// 动静拼接SQLStringBuilder sql = new StringBuilder();sql.append(" select  *   ");sql.append("   from user ");// 依据内部条件id动静拼接SQLif ( 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 ~