GVKun编程网logo

Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!(mybatis动态标签作用)

10

对于想了解Mybatis系列全解的读者,本文将提供新的信息,我们将详细介绍八:Mybatis的9大动态SQL标签你知道几个?提前致女神!,并且为您提供关于javaweb(七):mybatis的动态sq

对于想了解Mybatis系列全解的读者,本文将提供新的信息,我们将详细介绍八:Mybatis的9大动态SQL标签你知道几个?提前致女神!,并且为您提供关于java web(七): mybatis的动态sql和mybatis generator自动生成pojo类和映射文件、JavaEE_Mybatis_SpringMVC__Mybatis_lesson8_Mybatis的动态sql、JAVAEE——Mybatis加强:输入和输出映射、动态sql、关联查询、Mybatis整合spring、Mybatis逆向工程、mybatis 学习笔记(二):mybatis SQL注入问题的有价值信息。

本文目录一览:

Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!(mybatis动态标签作用)

Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!(mybatis动态标签作用)

Mybatis系列全解(八):Mybatis的9大动态sql标签你知道几个?提前致女神!

目录
  • Mybatis系列全解(八):Mybatis的9大动态SQL标签你知道几个?提前致女神!
      • 前言
      • Mybaits系列全解
      • 本文目录
        • 1、什么是动态SQL ?
        • 2、动态SQL的诞生记
        • 3、动态SQL标签的9大标签
          • Top1、if 标签
          • Top2、choose 标签、when 标签、otherwise 标签
          • Top3、foreach 标签
          • Top4、where 标签、set 标签
          • Top5、trim 标签
          • Top6、bind 标签
          • 拓展:sql标签 + include 标签
        • 4、动态SQL的底层原理
      • 总结

封面:洛小汐 作者:潘潘

img

img

2021年,仰望天空,脚踏实地。

img

img

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

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

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

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

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

前言

本节我们介绍 Mybatis 的强大特性之一:动态 sql ,从动态 sql 的诞生背景与基础概念,到动态 sql 的标签成员及基本用法,我们徐徐道来,再结合框架源码,剖析动态 sql (标签)的底层原理,最终在文末吐槽一下:在无动态 sql 特性(标签)之前,我们会常常掉进哪些可恶的坑吧~

img

建议关注我们! Mybatis 全解系列一直在更新哦

img

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的底层原理

img

1、什么是动态sql ?

关于动态 sql ,允许我们理解为 “ 动态的 sql ”,其中 “ 动态的 ” 是形容词,“ sql ” 是名词,那显然我们需要先理解名词,毕竟形容词仅仅代表它的某种形态或者某种状态。

sql 的全称是:

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

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

img

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

img

很显然,能够学习 Mybatis 这么一门高精尖(ru-men)持久层框架的编程人群,对于 sql 的编写能力肯定已经掌握得 ss 的,平时各种 sql 编写那都是信手拈来的事, 只不过对于 动态sql 到底是个什么东西,似乎还有一些朋友似懂非懂!但是没关系,我们百度一下。

动态 sql:一般指根据用户输入或外部条件 动态组合 的 sql 语句块。

很容易理解,随外部条件动态组合的 sql 语句块!我们先针对动态 sql 这个词来剖析,世间万物,有动态那就相对应的有静态,那么他们的边界在哪里呢?又该怎么区分呢?

img

其实,上面我们已经介绍过,在例如 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 。

img

另外,还有一种常见的情况,大家看看下面这个 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 独有的吗?

img

img

2、动态sql的诞生记

我们都知道,sql 是一种伟大的数据库语言 标准,在数据库管理系统纷争的时代,它的出现统一规范了数据库操作语言,而此时,市面上的数据库管理软件百花齐放,我最早使用的 SQL Server 数据库,当时用的数据库管理工具是 sql Server Management Studio,后来接触 Oracle 数据库,用了 PL/sql Developer,再后来直至今日就几乎都在用 MySQL 数据库(这个跟各种云厂商崛起有关),所以基本使用 Navicat 作为数据库管理工具,当然如今市面上还有许多许多,数据库管理工具嘛,只要能便捷高效的管理我们的数据库,那就是好工具,duck 不必纠结选择哪一款!

img

那这么多好工具,都提供什么功能呢?相信我们平时接触最多的就是接收执行 sql 语句的输入界面(也称为查询编辑器),这个输入界面几乎支持所有 sql 语法,例如我们编写一条语句查询 id 等于15 的用户数据记录:

select * from user where id = 15 ;

复制

我们来看一下这个查询结果:

img

很显然,在这个输入界面内输入的任何 sql 语句,对于数据库管理工具来说,都是 动态 sql!因为工具本身并不可能提前知道用户会输入什么 sql 语句,只有当用户执行之后,工具才接收到用户实际输入的 sql 语句,才能最终确定 sql 语句的主体结构,当然!即使我们不通过可视化的数据库管理工具,也可以用数据库本身自带支持的命令行工具来执行 sql 语句。但无论用户使用哪类工具,输入的语句都会被工具认为是 动态 sql

img

这么一说,动态 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!

img

于是乎,Mybatis 借助 OGNL 的表达式的伟大设计,可算在动态 sql 构建方面提供了各类功能强大的辅助标签,我们简单列举一下有:if、choose、when、otherwise、trim、where、set、foreach、bind等,我随手翻了翻我电脑里头曾经保存的学习笔记,我们一起在第3节中温故知新,详细的讲一讲吧~

img

另外,需要纠正一点,就是我们平日里在 Mybatis 框架中常说的动态 sql ,其实特指的也就是 Mybatis 框架中的这一套动态 sql 标签,或者说是这一 特性,而并不是在说动态 sql 本身。

img

3、动态sql标签的9大标签

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

img

当然,很多朋友会说这类需求,不能用 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 的维护上,既灵活也方便维护,可读性还强。

img

有些心细的朋友可能就发现一个问题,为什么 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>

复制

img

很明显,choose 标签作为多分支条件判断,提供了更多灵活的流程控制,同时 otherwise 的出现也为程序流程控制兜底,有时能够避免部分系统风险、过滤部分条件、避免当程序没有匹配到条件时,把整个数据库资源全部查询或更新。

至于为何 choose 标签这么棒棒,而常用度还是比 if 标签少了一颗星呢?原因也简单,因为 choose 标签的很多使用场景可以直接用 if 标签代替。另外据我统计,if 标签在实际业务应用当中,也要多于 choose 标签,大家也可以具体核查自己的应用程序中动态 sql 标签的占比情况,统计分析一下。


Top3、foreach 标签

常用度:★★★☆☆

实用性:★★★★☆

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

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

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

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

请问大家

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

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

img

额(⊙o⊙)…

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

img

没错,确实 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>

复制

img

可能你会觉得 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>

复制

img

延伸:当传入参数为多个对象时,例如传入 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>

复制

更多玩法,期待你自己去挖掘!

img

注意:使用 foreach 标签时,需要对传入的 collection 参数(List/Map/Set等)进行为空性判断,否则动态 sql 会出现语法异常,例如你的查询语句可能是 select * from user where ids in () ,导致以上语法异常就是传入参数为空,解决方案可以用 if 标签或 choose 标签进行为空性判断处理,或者直接在 Java 代码中进行逻辑处理即可,例如判断为空则不执行 sql 。


Top4、where 标签、set 标签

常用度:★★☆☆☆

实用性:★★★★☆

我们把 where 标签和 set 标签放置一起讲解,一是这两个标签在实际应用开发中常用度确实不分伯仲,二是这两个标签出自一家,都继承了 trim 标签,放置一起方便我们比对追根。(其中底层原理会在第4部分详细讲解)

img

之前我们介绍 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 标签也会将它替换去除。

img

了解了基本用法之后,我们再看看刚刚我们的例子中:

<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 子句元素没能被成功替换掉前缀,从而引起语法错误!

img

基于 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 标签都会将它替换去除。

img

根据此用法我们可以把以上的例子改为:

<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 标签当成末尾子句元素处理,导致后续真正的末尾子句元素的逗号没能被成功替换掉后缀,从而引起语法错误!

img

到此,我们的 where 标签元素与 set 标签就基本介绍完成,它俩确实极为相似,区别仅在于:

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

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


Top5、trim 标签

常用度:★☆☆☆☆

实用性:★☆☆☆☆

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

img

其实 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>

复制

大家应该大致能知道以上例子的功效,其实就是辅助构建模糊查询的语句拼接,那有人就好奇了,为啥不直接拼接语句就行了,为什么还要搞出一个变量,绕一圈呢?

img

我先问一个问题:平时你使用 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 标签语句片段。

img

简单的复用代码块可以是:

<!-- 可复用的字段语句块 -->
<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 属性或多层内部语句中使用属性值,属性可以穿透传递,例如:

img

<!-- 简单语句块 -->
<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节中详细分析。

img

img

4、动态sql的底层原理

想了解 Mybatis 究竟是如何解析与构建动态 sql ?首先推荐的当然是读源码,而读源码,是一个技术钻研问题,为了借鉴学习,为了工作储备,为了解决问题,为了让自己在编程的道路上跑得明白一些... 而希望通过读源码,去了解底层实现原理,切记不能脱离了整体去读局部,否则你了解到的必然局限且片面,从而轻忽了真核上的设计。如同我们读史或者观宇宙一样,最好的办法都是从整体到局部,不断放大,前后延展,会很舒服通透。所以我准备从 Mybatis 框架的核心主线上去逐步放大剖析。

img

通过前面几篇文章的介绍(建议阅读 Mybatis 系列全解之六:《Mybatis 最硬核的 API 你知道几个?》),其实我们知道了 Mybatis 框架的核心部分在于构件的构建过程,从而支撑了外部应用程序的使用,从应用程序端创建配置并调用 API 开始,到框架端加载配置并初始化构件,再创建会话并接收请求,然后处理请求,最终返回处理结果等。

img

我们的动态 sql 解析部分就发生在 sql 语句对象 MappedStatement 构建时(上左高亮橘色部分,注意观察其中 sql 语句对象与 sqlSource 、 Boundsql 的关系,在动态 sql 解析流程特别关键)。我们再拉近一点,可以看到无论是使用 XML 配置 sql 语句或是使用注解方式配置 sql 语句,框架最终都会把解析完成的 sql 语句对象存放到 MappedStatement 语句集合池子。

img

而以上虚线高亮部分,即是 XML 配置方式解析过程与注解配置方式解析过程中涉及到动态 sql 标签解析的流程,我们分别讲解:

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

img

以上为 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 语句,框架如何解析?

img

大家会发现注解配置方式的 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 开始讲起 ...

img

我们知道,无论 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 源实现)。

img

  • 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 字符串。

img

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 的执行过程。

img

墙裂推荐阅读之前第六文之 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。简单介绍一下:

img

  • 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 源对象是怎么被创建的吧!

img

通过以上的程序初始构建阶段,我们可以发现,最终语言驱动通过调用 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 作用仅是为了方便获取每一个语句的所有动态标签节点,于是应势而生。

img

知道动态 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 的底层解析过程我们基本讲解完,冗长了些,但流程上大致算完整,有遗漏的,我们回头再补充。

img

总结

不知不觉中,我又是这么巨篇幅的讲解剖析,确实不太适合碎片化时间阅读,不过话说回来,毕竟此文属于 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的复杂映射 》。

img

img

java web(七): mybatis的动态sql和mybatis generator自动生成pojo类和映射文件

java web(七): mybatis的动态sql和mybatis generator自动生成pojo类和映射文件

前言:

  MyBatis 的强大特性之一便是它的动态 SQL。如果你有使用 JDBC 或其它类似框架的经验,你就能体会到根据

不同条件拼接 SQL 语句的痛苦。例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。

利用动态 SQL 这一特性可以彻底摆脱这种痛苦。

  Mybatis Generator可以帮我们根据数据库表自动生成pojo类和SQL映射文件,SQL映射文件提供了增删改查功能。

 


         动态SQL

到网上找一个经典的mysql数据库表

CREATE TABLE `emp` (
  #编号
  `empno` int(11) NOT NULL,
  #姓名
  `ename` varchar(255) NOT NULL,
  #职位
  `job` varchar(255) DEFAULT NULL,
  `mgr` int(11) DEFAULT NULL,
  #入职时间
  `hiredate` date NOT NULL,
  #薪水
  `sal` decimal(7,2) DEFAULT NULL,
  #奖金级别
  `comm` decimal(7,2) DEFAULT NULL,
  #部门编号
  `deptno` int(11) DEFAULT NULL,
  PRIMARY KEY (`empno`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

#部门表
CREATE TABLE `dept` (
  #部门编号
  `deptno` int(11) NOT NULL,
  #部门名称
  `dname` varchar(255) NOT NULL,
  #部门地址
  `loc` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`deptno`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

 

INSERT INTO emp VALUES(7369,''SMITH'',''CLERK'',7902,''1980-12-17'',800,NULL,20);
INSERT INTO emp VALUES(7499,''ALLEN'',''SALESMAN'',7698,''1981-02-20'',1600,300,30);
INSERT INTO emp VALUES(7521,''WARD'',''SALESMAN'',7698,''1981-02-22'',1250,500,30);
INSERT INTO emp VALUES(7566,''JONES'',''MANAGER'',7839,''1981-04-02'',2975,NULL,20);
INSERT INTO emp VALUES(7654,''MARTIN'',''SALESMAN'',7698,''1981-09-28'',1250,1400,30);
INSERT INTO emp VALUES(7698,''BLAKE'',''MANAGER'',7839,''1981-05-01'',2850,NULL,30);
INSERT INTO emp VALUES(7782,''CLARK'',''MANAGER'',7839,''1981-06-09'',2450,NULL,10);
INSERT INTO emp VALUES(7788,''SCOTT'',''ANALYST'',7566,''1987-04-19'',3000,NULL,20);
INSERT INTO emp VALUES(7839,''KING'',''PRESIDENT'',NULL,''1981-11-17'',5000,NULL,10);
INSERT INTO emp VALUES(7844,''TURNER'',''SALESMAN'',7698,''1981-09-08'',1500,0,30);
INSERT INTO emp VALUES(7876,''ADAMS'',''CLERK'',7788,''1987-05-23'',1100,NULL,20);
INSERT INTO emp VALUES(7900,''JAMES'',''CLERK'',7698,''1981-12-03'',950,NULL,30);
INSERT INTO emp VALUES(7902,''FORD'',''ANALYST'',7566,''1981-12-03'',3000,NULL,20);
INSERT INTO emp VALUES(7934,''MILLER'',''CLERK'',7782,''1982-01-23'',1300,NULL,10);

 
INSERT INTO dept VALUES(10, ''ACCOUNTING'', ''NEW YORK'');
INSERT INTO dept VALUES(20, ''RESEARCH'', ''DALLAS'');
INSERT INTO dept VALUES(30, ''SALES'', ''CHICAGO'');
INSERT INTO dept VALUES(40, ''OPERATIONS'', ''BOSTON'');

 


 

表准备完成之后,开始编写动态SQL语句

If

动态 SQL 通常要做的事情是根据条件包含 where 子句的一部分。

<select id="selectEmployeeByCondition1" parameterType="Employee"
            resultMap="EmployeeBaseMap">
        select * from emp
        where 1 = 1
        <if test="empno != null">
            and empno = #{empno}
        </if>
        <if test="ename != null">
            and ename like #{ename}
        </if>
        <if test="job != null">
            and job = #{job}
        </if>
</select>

 这条语句会根据empno,ename,job是否为空插入条件, " where 1 =  1 "是为了避免3个条件都为空出现"select * from emp where "这种SQL语句。

每个if条件之间必须显式用and或or进行连接。

 

choose,when,otherwise

有时我们不想应用到所有的条件语句,而只想从中择其一项。针对这种情况,MyBatis 提供了 choose 元素

choose类似if else if  else,只会选择中一个条件执行。

  <!-- choose -->
    <select id="selectEmployeeByCondition2" parameterType="Employee" 
            resultMap="EmployeeBaseMap">
        select * from emp
        where
        <choose>
            <when test="empno != null">
                empno = #{empno}
            </when>
            <when test="ename">
                ename like #{ename}
            </when>
            <when test="job != null">
                job = #{job}
            </when>
            <otherwise>
                1 = 1
            </otherwise>
        </choose>
    </select>

 如果三个when条件都不满足,则会选中<otherwise>拼接到where条件后面!

 

where   set   trim

上面的几条动态SQL语句都会加上where条件,有时候我们希望如果条件都不满足的情况下不加where字句。

  <select id="selectEmployeeByCondition3" parameterType="Employee" 
            resultMap="EmployeeBaseMap">
        select * from emp
        <where>
            <if test="empno != null">
                and empno = #{empno}
            </if>
            <if test="ename != null">
                and ename like #{ename}
            </if>
            <if test="job != null">
                and job = #{job}
            </if>
        </where>
  </select> 

 

where 元素只会在至少有一个子元素的条件返回 SQL 子句的情况下才去插入“WHERE”子句。

而且,若语句的开头为“AND”或“OR”,where 元素也会将它们去除。

 

和where等价的trim

  <select id="selectEmployeeByCondition4" parameterType="Employee" 
            resultMap="EmployeeBaseMap">
        select * from emp
        <trim prefix="where" prefixOverrides="and | or">
            <if test="empno != null">
                and empno = #{empno}
            </if>
            <if test="ename != null">
                and ename like #{ename}
            </if>
            <if test="job != null">
                and job = #{job}
            </if>
        </trim>
    </select> 

 

 <trim> 标签有两个属性,prefix是指定插入的内容,prefixOverrides属性会忽略通过管道分隔的文本序列。

它的作用是移除所有指定在 prefixOverrides 属性中的内容,并且插入 prefix 属性中指定的内容。

假设一和三条件满足,SQL语句就会变成 " select * from emp where empno = #{empno} and job = #{job} ";

 

<set>标签是专门为更新语句而准备的。

  <update id="updateEmployee" parameterType="Employee">
        update emp
        <set>
            <if test="ename != null">
                ename = #{ename},
            </if>
            <if test="job != null">
                job = #{job},
            </if>
            <if test="mgr != null">
                mgr = #{mgr},
            </if>
            <if test="salary != null">
                sal = #{salary},
            </if>
            <if test="comment != null">
                comm = #{comment},
            </if>
            <if test="dept != null and dept.deptNo != null">
                deptno = #{dept.deptNo}
            </if>
        </set>
        where empno = #{empno}
    </update>

 set 元素可以用于动态包含需要更新的列,而舍去其它的

这里,set 元素会动态前置 SET 关键字,同时也会删掉无关的逗号,

因为用了条件语句之后很可能就会在生成的 SQL 语句的后面留下这些逗号。

当然trim也可以完成set标签的功能

  <update id="updateEmployee2" parameterType="Employee">
        update emp
        <trim prefix="set" prefixOverrides=",">
            <if test="ename != null">
                ename = #{ename},
            </if>
            <if test="job != null">
                job = #{job},
            </if>
            <if test="mgr != null">
                mgr = #{mgr},
            </if>
            <if test="salary != null">
                sal = #{salary},
            </if>
            <if test="comment != null">
                comm = #{comment},
            </if>
            <if test="dept != null and dept.deptNo != null">
                deptno = #{dept.deptNo}
            </if>
        </trim>
        where empno = #{empno}
    </update>

 

forEach

动态 SQL 的另外一个常用的操作需求是对一个集合进行遍历,通常是在构建 IN 条件语句的时候。

  <select id="selectEmployeeByEmpnos" parameterType="list" resultMap="EmployeeBaseMap">
        select * from emp
        where empno in
        <foreach collection="list" open="(" close=")" item="item" index="index" separator=",">
            #{item}
        </foreach>
    </select>

 foreach 元素的功能非常强大,它允许你指定一个集合,声明可以在元素体内使用的集合项(item)和

索引(index)变量。它也允许你指定开头与结尾的字符串以及在迭代结果之间放置分隔符。

这个元素是很智能的,因此它不会偶然地附加多余的分隔符。

注意: 你可以将任何可迭代对象(如 List、Set 等)、Map 对象或者数组对象传递给 foreach 作为集合参数。

当使用可迭代对象或者数组时,index 是当前迭代的次数,item 的值是本次迭代获取的元素。

当使用 Map 对象(或者 Map.Entry 对象的集合)时,index 是键,item 是值。

 

动态SQL介绍完毕,这里提供一下完整的映射文件和映射接口。

映射文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.briup.mapper.DynamicMapper">
    <!-- 告诉数据库如何加载数据到类的那个属性上 -->
    <resultMap type="Employee" id="EmployeeBaseMap">
        <id column="empno" property="empno"/>
        <result column="ename" property="ename"/>
        <result column="job" property="job"/>
        <result column="mgr" property="mgr"/>
        <result column="hiredate" property="hiredate"/>
        <result column="sal" property="salary"/>
        <result column="comm" property="comment"/>
    </resultMap>
    
    <resultMap type="Deptarment" id="DeptarmentBaseMap">
        <id column="deptno" property="deptNo"/>
        <result column="dname" property="dName"/>
        <result column="loc" property="loc"/>
    </resultMap>
    
    
    <!-- if -->
    <select id="selectEmployeeByCondition1" parameterType="Employee" 
            resultMap="EmployeeBaseMap">
        select * from emp
        where 1 = 1
        <if test="empno != null">
            and empno = #{empno}
        </if>
        <if test="ename != null">
            and ename like #{ename}
        </if>
        <if test="job != null">
            and job = #{job}
        </if>
    </select>
    
    <!-- choose -->
    <select id="selectEmployeeByCondition2" parameterType="Employee" 
            resultMap="EmployeeBaseMap">
        select * from emp
        where
        <choose>
            <when test="empno != null">
                empno = #{empno}
            </when>
            <when test="ename">
                ename like #{ename}
            </when>
            <when test="job != null">
                job = #{job}
            </when>
            <otherwise>
                1 = 1
            </otherwise>
        </choose>
    </select>
    
    <!-- where trim set -->
    <select id="selectEmployeeByCondition3" parameterType="Employee" 
            resultMap="EmployeeBaseMap">
        select * from emp
        <where>
            <if test="empno != null">
                and empno = #{empno}
            </if>
            <if test="ename != null">
                and ename like #{ename}
            </if>
            <if test="job != null">
                and job = #{job}
            </if>
        </where>
    </select> 
    
    <select id="selectEmployeeByCondition4" parameterType="Employee" 
            resultMap="EmployeeBaseMap">
        select * from emp
        <trim prefix="where" prefixOverrides="and | or">
            <if test="empno != null">
                and empno = #{empno}
            </if>
            <if test="ename != null">
                and ename like #{ename}
            </if>
            <if test="job != null">
                and job = #{job}
            </if>
        </trim>
    </select> 
    
    <update id="updateEmployee" parameterType="Employee">
        update emp
        <set>
            <if test="ename != null">
                ename = #{ename},
            </if>
            <if test="job != null">
                job = #{job},
            </if>
            <if test="mgr != null">
                mgr = #{mgr},
            </if>
            <if test="salary != null">
                sal = #{salary},
            </if>
            <if test="comment != null">
                comm = #{comment},
            </if>
            <if test="dept != null and dept.deptNo != null">
                deptno = #{dept.deptNo}
            </if>
        </set>
        where empno = #{empno}
    </update>
    
    <update id="updateEmployee2" parameterType="Employee">
        update emp
        <trim prefix="set" prefixOverrides=",">
            <if test="ename != null">
                ename = #{ename},
            </if>
            <if test="job != null">
                job = #{job},
            </if>
            <if test="mgr != null">
                mgr = #{mgr},
            </if>
            <if test="salary != null">
                sal = #{salary},
            </if>
            <if test="comment != null">
                comm = #{comment},
            </if>
            <if test="dept != null and dept.deptNo != null">
                deptno = #{dept.deptNo}
            </if>
        </trim>
        where empno = #{empno}
    </update>
    
    <select id="selectEmployeeByEmpnos" parameterType="list" resultMap="EmployeeBaseMap">
        select * from emp
        where empno in
        <foreach collection="list" open="(" close=")" item="item" index="index" separator=",">
            #{item}
        </foreach>
    </select>
</mapper>
View Code

 映射接口:

package com.briup.mapper;

import java.util.List;

import com.briup.bean.Employee;

public interface DynamicMapper {
    
    public abstract List<Employee> selectEmployeeByCondition1(Employee emp);
    
    public abstract List<Employee> selectEmployeeByCondition2(Employee emp);
    
    public abstract List<Employee> selectEmployeeByCondition3(Employee emp);
    
    public abstract List<Employee> selectEmployeeByCondition4(Employee emp);
    
    public abstract void updateEmployee(Employee emp);
    
    public abstract List<Employee> selectEmployeeByEmpnos(List<Integer> empnos);
}
View Code

 

 


 

mybatis Generator在ecplise中的使用

mybatis生成器(mbg)是mybatis mybatis和ibatis的代码生成器。它将为mybatis的所有版本生成代码,

并在版本2.2.0之后生成ibatis的版本。它将内省一个数据库表(或多个表),并生成可用于访问表的工件。

这减少了设置对象和配置文件以与数据库表交互的初始麻烦。MBG试图对大部分简单的CRUD(创建、检索、更新、删除)

数据库操作产生重大影响。您仍然需要为连接查询或存储过程手工编写SQL和对象代码。

MyBatis Generator will generate:

         *Java POJOs that match the table structure.

         *MyBatis/iBATIS Compatible SQL Map XML Files. MBG generates SQL for simple CRUD    functions on each table in a configuration.

         *Java client classes that make appropriate use of the above objects.

 

1.安装Mybatis Generator插件

  help --> Ecplise Marketplace...

然后搜索mybats,点击安装Mybatis Generator 1.3.7

 

2.创建配置文件

  右键点击java项目,选择 " New -- > Other... " 然后搜索mybatis,选中Mybatis Generator configuration File。

3.准备一些包和目录

在项目下创建一个目录lib,把连接mysql数据库的jar包放进去,我这里是msql-connector-java-5.1.47.jar

创建包com.briup.bean 和 com.briup.mapper,等下自动生成的pojo类和映射文件会放到这下面。

4.修改配置文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
  PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
  "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
<!-- mybatis-generator的核心配置文件 -->
<generatorConfiguration>
    <!-- 连接数据库用到的jar包 -->
    <classPathEntry
        location="G:\java-code-2\mybatis-generator\lib\mysql-connector-java-5.1.47.jar" />

    <context id="DB2Tables" targetRuntime="MyBatis3">
        <jdbcConnection driverClass="com.mysql.jdbc.Driver"
            connectionURL="jdbc:mysql://188.131.246.182:3306/cnblogs" userId="study"
            password="123456">
            <!-- 生成primary key方法 -->
            <property name="userInformationSchema" value="true" />
        </jdbcConnection>

        <!--指定生成的类型为java类型,避免数据库中number等类型字段 -->
        <javaTypeResolver>
            <property name="forceBigDecimals" value="false" />
        </javaTypeResolver>

        <!--自动生成的实体的存放包路径 -->
        <javaModelGenerator targetPackage="com.briup.bean"
            targetProject="mybatis-generator/src">
            <property name="enableSubPackages" value="true" />
            <property name="trimStrings" value="true" />
        </javaModelGenerator>

        <!--自动生成的*Mapper.xml文件存放路径 -->
        <sqlMapGenerator targetPackage="com.briup.mapper"
            targetProject="mybatis-generator/src">
            <property name="enableSubPackages" value="true" />
        </sqlMapGenerator>

        <!--自动生成的*Mapper.java存放路径 -->
        <javaClientGenerator type="XMLMAPPER"
            targetPackage="com.briup.mapper" targetProject="mybatis-generator/src">
            <property name="enableSubPackages" value="true" />
        </javaClientGenerator>


        <!-- 映射配置 -->
        <table tableName="emp" domainObjectName="Employee"></table>
        <table tableName="dept" domainObjectName="Deptarment"></table>
    </context>
</generatorConfiguration>

 5.运行配置文件

右键点击generatorConfig.xml文件,选择 ''Run As'' , ''Run Mybatis Generator''。

 

6.简单的介绍如何使用Mybatis Generator生成的映射文件进行增删改查。

如果你去看xml文件,会发现里面使用了动态的SQL语句,不过又额外的增加了一些东西。有兴趣的可以研究一下。

使用之前还需要把环境搭建好,什么mybatis-config.xml全局配置文件以及其他的都要准备好,上一篇随笔有如何在ecplise搭建一个mybatis项目。

由于自动生成的pojo类中toString都没有重写,不好看结果,我利用ecplise自动生成toString方法。

 

0)使用案例0

@SuppressWarnings("unused")
    @Test
    public void test0() {
        SqlSession session = null;
        try {
            session = MySqlSessionFactory.opensession();
            EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
            
            // 查询所有员工信息
            EmployeeExample example = new EmployeeExample();
            List<Employee> list = mapper.selectByExample(example);
            System.out.println("list.size(): " + list.size());
            for(Employee e: list)
                System.out.println("name: " + e.getEname());
        } catch (IOException e) {
            if(session != null)
                session.close();
            e.printStackTrace();
        }
    }
View Code

1)使用案列1

查询工资大于1200的员工姓名和工资

@SuppressWarnings("unused")
    @Test
    public void test1() {
        SqlSession session = null;
        try {
            session = MySqlSessionFactory.opensession();
            EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
            
            // 查询工资大于1200的员工姓名和工资 
            EmployeeExample example = new EmployeeExample();
            Criteria criteria = example.createCriteria();
            // 添加查询条件
            criteria.andSalGreaterThan(new BigDecimal(1200));
            List<Employee> list = mapper.selectByExample(example);
            System.out.println("list.size(): " + list.size() + " and list: " + list );
        } catch (IOException e) {
            if(session != null)
                session.close();
            e.printStackTrace();
        }
    }
View Code

  2)使用案列2

选择工资不在500到1200的员工的姓名和工资

@SuppressWarnings("unused")
    @Test
    public void test2() {
        SqlSession session = null;
        try {
            session = MySqlSessionFactory.opensession();
            EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
            
            // 选择工资不在500到1200的员工的姓名和工资
            EmployeeExample example = new EmployeeExample();
            Criteria criteria = example.createCriteria();
            // 添加查询条件
            criteria.andSalNotBetween(new BigDecimal(500), new BigDecimal(1200));
            List<Employee> list = mapper.selectByExample(example);
            System.out.println("list.size(): " + list.size());
            for(Employee e: list)
                System.out.println("salary: " + e.getSal());
        } catch (IOException e) {
            if(session != null)
                session.close();
            e.printStackTrace();
        }
    }
View Code

 3)使用案例3

选择姓名中有字母a和e的员工

@SuppressWarnings({ "unused" })
    @Test
    public void test3() {
        SqlSession session = null;
        try {
            session = MySqlSessionFactory.opensession();
            EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
            
            // 选择姓名中有字母a和e的员工
            EmployeeExample example = new EmployeeExample();
            Criteria criteria = example.createCriteria();
            // 添加查询条件
            criteria.andEnameLike("%a%");
            criteria.andEnameLike("%e%");
            List<Employee> list = mapper.selectByExample(example);
            System.out.println("list.size(): " + list.size());
            for(Employee e: list)
                System.out.println("name: " + e.getEname());
        } catch (IOException e) {
            if(session != null)
                session.close();
            e.printStackTrace();
        }
    }
View Code

 

  注:如何需要联合查询还需要自己写sql语句,mybatsi Generator生成的SQL映射文件只提供增删改查功能。

备注:mybais generator项目已经在百度网盘分享。

地址:https://pan.baidu.com/s/1ZrFsu1CcMtrBCmvk9y5IDQ

提取码:2tk9

 

JavaEE_Mybatis_SpringMVC__Mybatis_lesson8_Mybatis的动态sql

JavaEE_Mybatis_SpringMVC__Mybatis_lesson8_Mybatis的动态sql

项目代码

http://pan.baidu.com/s/1c01BLvi


Mybatis中  拼接动态SQL,让SQL根据传递进来的参数,进行动态的拼接,拼成指定条件的语句


0.配置文件

1.userMapper.java (Dao接口)

2.userMapper.xml  (Dao实现)   1,2是通过配置文件中 批量加载mapper 的方式联系的。(package的方式) 

注意

(1) xxxMapper.xml 需要与 xxxMapper.java 名称相同,

(2)且放到同一文件夹下面。

动态SQL写在 userMapper.xml 里面:

  通过类似于JSTL标签的表现方式

<!-- where 可以自动去掉第一个and  -->

	<!--
	通过OGNL的方式进行获取 
	 -->
	<select id="findUserList" parameterType="cn.itcast.mybatis.po.UserQueryVo"
	resultType="cn.itcast.mybatis.po.UserCustom">
		SELECT * FROM user 
		<!--
		where 可以自动去掉第一个and 
		 -->
		<where>
			<if test="userCustom!=null">
				<if test="userCustom.sex!=null and userCustom.sex!=''">
					AND user.sex = #{userCustom.sex}
				</if>
				<if test="userCustom.username!=null and userCustom.username!=''">
					AND user.username like'%${userCustom.username}%'
				</if>
			</if>
		</where>
	</select>



3.User.java (ORM)

4.UserCustom.java(po类)

5.UserQueryVo(vo类)

tips:

(1) Vo 视图层面的对象

(2) Po 持久层面的对象

(3) Pojo 用户自定义的对象,综合了Vo与Po的JavaBean

6.junit 单元测试




文档目录结构



0.配置文件

db.properties

[plain]  view plain copy print ?
  1. jdbc.driver=com.mysql.jdbc.Driver  
  2. jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8  
  3. jdbc.username=root  
  4. jdbc.password=123456  



SqlMapConfig.xml

[plain]  view plain copy print ?
  1. <?xml version="1.0" encoding="UTF-8" ?>  
  2. <!DOCTYPE configuration  
  3. PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
  4. "http://mybatis.org/dtd/mybatis-3-config.dtd">  
  5. <configuration>  
  6.     <!-- 加载属性文件 -->  
  7.     <properties resource="db.properties"></properties>  
  8.     <!--   
  9.     <settings></settings>  
  10.      -->  
  11.     <typeAliases>  
  12.         <!--  
  13.         <typeAlias type="cn.itcast.mybatis.po.User" alias="user"/>   
  14.          -->  
  15.          <!-- 批量定义别名 -->  
  16.         <package name="cn.itcast.mybatis.po"/>  
  17.     </typeAliases>  
  18.     <!-- 和spring整合后 environments配置将废除-->  
  19.     <environments default="development">  
  20.         <environment id="development">  
  21.         <!-- 使用jdbc事务管理-->  
  22.             <transactionManager type="JDBC" />  
  23.         <!-- 数据库连接池-->  
  24.             <dataSource type="POOLED">  
  25.                 <property name="driver" value="${jdbc.driver}" />  
  26.                 <property name="url" value="${jdbc.url}" />  
  27.                 <property name="username" value="${jdbc.username}" />  
  28.                 <property name="password" value="${jdbc.password}" />  
  29.             </dataSource>  
  30.         </environment>  
  31.     </environments>  
  32.     <mappers>  
  33.         <mapper resource="sqlmap/User.xml"/>  
  34.         <!--   
  35.         <mapper resource="mapper/UserMapper.xml"/>  
  36.          -->  
  37.         <!--  
  38.         <mapper />   
  39.          -->  
  40.         <package name="cn.itcast.mybatis.mapper"/>  
  41.           
  42.     </mappers>  
  43. </configuration>  


1.userMapper.java (Dao接口)

[plain]  view plain copy print ?
  1. package cn.itcast.mybatis.mapper;  
  2.   
  3. import java.util.List;  
  4.   
  5. import cn.itcast.mybatis.po.User;  
  6. import cn.itcast.mybatis.po.UserCustom;  
  7. import cn.itcast.mybatis.po.UserQueryVo;  
  8.   
  9. public interface UserMapper {  
  10.   
  11.     List<UserCustom> findUserList(UserQueryVo userQueryVo) throws Exception;  
  12.   
  13.     User findUserById(Integer id) throws Exception;  
  14.   
  15.     public List<User> findUserByName(String name) throws Exception;  
  16.   
  17.     void insertUser(User user) throws Exception;  
  18.   
  19.     void deleteUser(int id) throws Exception;  
  20.   
  21. }  


2.userMapper.xml  (Dao实现)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- namespace命名空间,作用就是对sql进行分类化管理,理解sql隔离 
注意:使用mapper代理方法开发,namespace有特殊重要的作用-->
<mapper namespace="cn.itcast.mybatis.mapper.UserMapper">

	<resultMap type="User" id="userResultMap">
		<id column="_id" property="id"/>
		<result column="_username" property="username"/>
	</resultMap>
	
	<!--
	通过OGNL的方式进行获取 
	 -->
	<select id="findUserList" parameterType="cn.itcast.mybatis.po.UserQueryVo"
	resultType="cn.itcast.mybatis.po.UserCustom">
		SELECT * FROM user 
		<!--
		where 可以自动去掉第一个and 
		 -->
		<where>
			<if test="userCustom!=null">
				<if test="userCustom.sex!=null and userCustom.sex!=''">
					AND user.sex = #{userCustom.sex}
				</if>
				<if test="userCustom.username!=null and userCustom.username!=''">
					AND user.username like'%${userCustom.username}%'
				</if>
			</if>
		</where>
	</select>

	<select id="findUserCount" parameterType="cn.itcast.mybatis.po.UserQueryVo"
	resultType="int">
		SELECT COUNT(*) FROM user WHERE user.sex = #{userCustom.sex} AND user.username like'%${userCustom.username}%'
		<!--
		where 可以自动去掉第一个and 
		 -->
		<where>
			<if test="userCustom!=null">
				<if test="userCustom.sex!=null and userCustom.sex!=''">
					AND user.sex = #{userCustom.sex}
				</if>
				<if test="userCustom.username!=null and userCustom.username!=''">
					AND user.username like'%${userCustom.username}%'
				</if>
			</if>
		</where>
	</select>
	
	<!-- 利用resultMap作为返回值返回参数 -->
	<select id="findUserByIdResultMap" parameterType="java.lang.Integer" resultMap="userResultMap">
	 	SELECT id _id, username _username FROM user where id = #{id} 
	</select>
	
	<!-- 在 映射文件中配置很多sql语句 -->
	<!-- 需求:通过id查询用户表的记录 -->
	<!-- 通过 select执行数据库查询
	id:标识 映射文件中的 sql
	将sql语句封装到mappedStatement对象中,
	=====================所以将id称为statement的id
	parameterType:指定输入 参数的类型,这里指定int型 
	#{}表示一个占位符号
	
	#{id}:其中的
	====================id表示接收输入 的参数,参数名称就是id,
	====================如果输入 参数是简单类型,#{}中的参数名可以任意,可以value或其它名称
	#{},
	resultType:指定sql输出结果 的所映射的java对象类型,select指定resultType表示将单条记录映射成的java对象。
	 -->
	 <!-- cn.itcast.mybatis.po.User -->
	 <select id="findUserById" parameterType="java.lang.Integer" resultType="user">
	 	SELECT * FROM user where id = #{id} 
	 </select>
	 
	 
	 
	<!-- 根据用户名称模糊查询用户信息,可能返回多条
	resultType:指定就是单条记录所映射的java对象 类型
	${}:表示拼接sql串,将接收到参数的内容不加任何修饰拼接在sql中。
	=============使用${}拼接sql,引起 sql注入
	=============${value}:接收输入 参数的内容,如果传入类型是简单类型,${}中只能使用value
	 --> 
	 <select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User">
	 	SELECT * FROM user where username like '%${value}%'
	 </select>
	 
	 
	 <insert id="insertUser" parameterType="cn.itcast.mybatis.po.User">
	 	<!-- 
		将插入数据的主键返回,返回到user对象中
		
		=================SELECT LAST_INSERT_ID():得到刚insert进去记录的主键值,只适用与自增主键
		
		keyProperty:将查询到主键值设置到parameterType指定的对象的哪个属性
		orderSELECT LAST_INSERT_ID()执行顺序,相对于insert语句来说它的执行顺序
		resultType:指定SELECT LAST_INSERT_ID()的结果类型
		 -->
	 	<selectKey keyProperty="id" order="AFTER" resultType="java.lang.Integer">
	 		SELECT LAST_INSERT_ID()
	 	</selectKey>
	 	INSERT INTO user(username,birthday,sex,address) VALUES(#{username},#{birthday},#{sex},#{address}) 
	 	<!-- 
		使用mysql的uuid()生成主键
		执行过程:
		首先通过uuid()得到主键,将主键设置到user对象的id属性中
		其次在insert执行时,从user对象中取出id属性值
		 -->
		<!--  <selectKey keyProperty="id" order="BEFORE" resultType="java.lang.String">
			SELECT uuid()
		</selectKey>
		insert into user(id,username,birthday,sex,address) value(#{id},#{username},#{birthday},#{sex},#{address}) -->
	 </insert>
	 	
	<!-- 删除 用户
	根据id删除用户,需要输入 id值
	 -->
	<delete id="deleteUser" parameterType="java.lang.Integer">
		DELETE FROM user WHERE id=#{id}
	</delete>
	 
	<!-- 根据id更新用户
	分析:
	需要传入用户的id
	需要传入用户的更新信息
	parameterType指定user对象,包括 id和更新信息,注意:id必须存在
	#{id}:从输入 user对象中获取id属性值
	 -->
	<update id="updateUser"	parameterType="cn.itcast.mybatis.po.User">
		UPDATE user SET username=#{username},birthday=#{birthday},sex=#{sex},address=#{address} 
		WHERE id=#{id}
	</update>
	 
</mapper>



3.User.java (ORM)

[java]  view plain copy print ?
  1. package cn.itcast.mybatis.po;  
  2.   
  3. import java.util.Date;  
  4.   
  5. //hibernate字段名和属性名相对应  
  6. public class User {  
  7.     @Override  
  8.     public String toString() {  
  9.         return "User [id=" + id + ", username=" + username + ", birthday="  
  10.                 + birthday + ", sex=" + sex + ", address=" + address + "]";  
  11.     }  
  12.   
  13.     private int id;  
  14.     private String username;  
  15.     private Date birthday;  
  16.     private String sex;  
  17.     private String address;  
  18.   
  19.     public int getId() {  
  20.         return id;  
  21.     }  
  22.   
  23.     public void setId(int id) {  
  24.         this.id = id;  
  25.     }  
  26.   
  27.     public String getUsername() {  
  28.         return username;  
  29.     }  
  30.   
  31.     public void setUsername(String username) {  
  32.         this.username = username;  
  33.     }  
  34.   
  35.     public Date getBirthday() {  
  36.         return birthday;  
  37.     }  
  38.   
  39.     public void setBirthday(Date birthday) {  
  40.         this.birthday = birthday;  
  41.     }  
  42.   
  43.     public String getSex() {  
  44.         return sex;  
  45.     }  
  46.   
  47.     public void setSex(String sex) {  
  48.         this.sex = sex;  
  49.     }  
  50.   
  51.     public String getAddress() {  
  52.         return address;  
  53.     }  
  54.   
  55.     public void setAddress(String address) {  
  56.         this.address = address;  
  57.     }  
  58.   
  59. }  



4.UserCustom.java(po类)

[java]  view plain copy print ?
  1. package cn.itcast.mybatis.po;  
  2.   
  3. /** 
  4.  * <p> 
  5.  * Description:用户的扩展类 
  6.  * </p> 
  7.  *  
  8.  * @author szh 
  9.  */  
  10. public class UserCustom extends User {  
  11.     // 可以扩展用户的信息  
  12. }  




5.UserQueryVo(vo类)

[java]  view plain copy print ?
  1. package cn.itcast.mybatis.po;  
  2.   
  3. /** 
  4.  *  
  5.  * @author szh 
  6.  *  
  7.  */  
  8. public class UserQueryVo {  
  9.     private UserCustom userCustom;  
  10.   
  11.     public UserCustom getUserCustom() {  
  12.         return userCustom;  
  13.     }  
  14.   
  15.     public void setUserCustom(UserCustom userCustom) {  
  16.         this.userCustom = userCustom;  
  17.     }  
  18.   
  19.     // 可以包装其他的查询条件,订单,商品  
  20. }  


6.junit 单元测试

package cn.itcast.mybatis.mapper;
  • import java.io.InputStream;
  • import java.util.List;
  • import org.apache.ibatis.io.Resources;
  • import org.apache.ibatis.session.SqlSession;
  • import org.apache.ibatis.session.SqlSessionFactory;
  • import org.apache.ibatis.session.SqlSessionFactoryBuilder;
  • import org.junit.Before;
  • import org.junit.Test;
  • import cn.itcast.mybatis.po.User;
  • import cn.itcast.mybatis.po.UserCustom;
  • import cn.itcast.mybatis.po.UserQueryVo;
  • public class UserMapperTest {
  • private SqlSessionFactory sqlSessionFactory;
  • // 此方法是在执行testFindUserById之前执行
  • @Before
  • public void setUp() throws Exception {
  • String resource = null; // mybatis全局配置文件
  • InputStream inputStream = null; // 输入流
  • try {
  • // mybatis配置文件
  • resource = "SqlMapConfig.xml";
  • inputStream = Resources.getResourceAsStream(resource);
  • // 创建会话工厂,传入mybatis配置文件信息
  • // 创建sqlSessionFactory
  • this.sqlSessionFactory = new SqlSessionFactoryBuilder()
  • .build(inputStream);
  • } catch (Exception e) {
  • e.printStackTrace();
  • }
  • }
  • @Test
  • public void testFindUserById() throws Exception {
  • SqlSession sqlSession = sqlSessionFactory.openSession();
  • UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  • User user = userMapper.findUserById(1);
  • System.out.println(user);
  • sqlSession.close();
  • }
  • @Test
  • public void testFindUserByIdResultMap() throws Exception {
  • SqlSession sqlSession = sqlSessionFactory.openSession();
  • UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  • User user = userMapper.findUserByIdResultMap(1);
  • System.out.println(user);
  • sqlSession.close();
  • }
  • @Test
  • public void testFindUserByName() throws Exception {
  • SqlSession sqlSession = sqlSessionFactory.openSession();
  • // 创建UserMapper对象,mybatis自动生成mapper代理对象
  • UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  • List<User> userList = userMapper.findUserByName("小");
  • System.out.println(userList);
  • sqlSession.close();
  • }
  • @Test
  • public void testFindUserList() throws Exception {
  • SqlSession sqlSession = null;
  • try {
  • sqlSession = sqlSessionFactory.openSession();
  • // 创建UserMapper对象,mybatis 自动生成mapper代理对象
  • UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  • UserQueryVo userQueryVo = new UserQueryVo();
  • UserCustom userCustom = new UserCustom();
  • // userCustom.setSex("1");
  • // userCustom.setUsername("小");
  • userQueryVo.setUserCustom(userCustom);
  • // userQueryVo
  • List<UserCustom> list = userMapper.findUserList(null);
  • System.out.println(list);
  • } catch (Exception e) {
  • e.printStackTrace();
  • } finally {
  • sqlSession.close();
  • }
  • }
  • @Test
  • public void testFindUserCount() throws Exception {
  • SqlSession sqlSession = null;
  • try {
  • sqlSession = sqlSessionFactory.openSession();
  • // 创建UserMapper对象,mybatis 自动生成mapper代理对象
  • UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  • UserQueryVo userQueryVo = new UserQueryVo();
  • UserCustom userCustom = new UserCustom();
  • userCustom.setSex("1");
  • userCustom.setUsername("小");
  • userQueryVo.setUserCustom(userCustom);
  • Integer count = userMapper.findUserCount(userQueryVo);
  • System.out.println(count);
  • } catch (Exception e) {
  • e.printStackTrace();
  • } finally {
  • sqlSession.close();
  • }
  • }
  • @Test
  • public void testInsertUser() throws Exception {
  • SqlSession sqlSession = null;
  • try {
  • sqlSession = sqlSessionFactory.openSession();
  • // 创建UserMapper对象,mybatis自动生成mapper代理对象
  • UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  • User user = new User();
  • user.setUsername("hahak");
  • userMapper.insertUser(user);
  • sqlSession.commit();
  • } catch (Exception e) {
  • e.printStackTrace();
  • } finally {
  • sqlSession.close();
  • }
  • }
  • @Test
  • public void testDeleteUser() throws Exception {
  • SqlSession sqlSession = null;
  • try {
  • sqlSession = sqlSessionFactory.openSession();
  • // 创建UserMapper对象,mybatis 自动生成mapper代理对象
  • UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
  • userMapper.deleteUser(36);
  • sqlSession.commit();
  • } catch (Exception e) {
  • e.printStackTrace();
  • } finally {
  • sqlSession.close();
  • }
  • }
  • }


  • 重要测试代码:

    	@Test
    	public void testFindUserList() throws Exception {
    		SqlSession sqlSession = null;
    		try {
    			sqlSession = sqlSessionFactory.openSession();
    			// 创建UserMapper对象,mybatis 自动生成mapper代理对象
    			UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    			UserQueryVo userQueryVo = new UserQueryVo();
    			UserCustom userCustom = new UserCustom();
    			// userCustom.setSex("1");
    			// userCustom.setUsername("小");
    			userQueryVo.setUserCustom(userCustom);
    
    			// userQueryVo
    			List<UserCustom> list = userMapper.findUserList(null);
    			System.out.println(list);
    		} catch (Exception e) {
    			e.printStackTrace();
    		} finally {
    			sqlSession.close();
    		}
    	}
    
    	@Test
    	public void testFindUserCount() throws Exception {
    		SqlSession sqlSession = null;
    		try {
    			sqlSession = sqlSessionFactory.openSession();
    			// 创建UserMapper对象,mybatis 自动生成mapper代理对象
    			UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
    			UserQueryVo userQueryVo = new UserQueryVo();
    			UserCustom userCustom = new UserCustom();
    			userCustom.setSex("1");
    			userCustom.setUsername("小");
    			userQueryVo.setUserCustom(userCustom);
    
    			Integer count = userMapper.findUserCount(userQueryVo);
    			System.out.println(count);
    		} catch (Exception e) {
    			e.printStackTrace();
    		} finally {
    			sqlSession.close();
    		}
    	}



    单元测试结果

    对   List<UserCustom> findUserList(UserQueryVo userQueryVo) throws Exception; 进行的测试






    JAVAEE——Mybatis加强:输入和输出映射、动态sql、关联查询、Mybatis整合spring、Mybatis逆向工程

    JAVAEE——Mybatis加强:输入和输出映射、动态sql、关联查询、Mybatis整合spring、Mybatis逆向工程

    1. 学习计划

    1、输入映射和输出映射

    a) 输入参数映射

    b) 返回值映射

    2、动态sql

    a) If标签

    b) Where标签

    c) Sql片段

    d) Foreach标签

    3、关联查询

    a) 一对一关联

    b) 一对多关联

    4、Mybatis整合spring

    a) 如何整合spring

    b) 使用原始的方式开发dao

    c) 使用Mapper接口动态代理

    5、Mybatis逆向工程(掌握)

    2. 输入映射和输出映射

    Mapper.xml映射文件中定义了操作数据库的sql,每个sql是一个statement,映射文件是mybatis的核心。

    2.1. 环境准备

    1. 复制昨天的工程,按照下图进行

     

     

    2.如下图粘贴,并更名

     

     

     3.只保留Mapper接口开发相关的文件,其他的删除

    最终效果如下图:

     

     

     4.如下图修改SqlMapConfig.xml配置文件。Mapper映射器只保留包扫描的方式

     

     

    2.2. parameterType(输入类型)

    2.2.1. 传递简单类型

    参考第一天内容。

    使用#{}占位符,或者${}进行sql拼接。

     

    2.2.2. 传递pojo对象

    参考第一天的内容。

    Mybatis使用ognl表达式解析对象字段的值,#{}或者${}括号中的值为pojo属性名称。

     

    2.2.3. 传递pojo包装对象

    开发中通过可以使用pojo传递查询条件。

    查询条件可能是综合的查询条件,不仅包括用户查询条件还包括其它的查询条件(比如查询用户信息的时候,将用户购买商品信息也作为查询条件),这时可以使用包装对象传递输入参数。

    包装对象:Pojo类中的一个属性是另外一个pojo

     

    需求:根据用户名模糊查询用户信息,查询条件放到QueryVouser属性中。

     

    2.2.3.1. 编写QueryVo

    public class QueryVo {
        // 包含其他的pojo
        private User user;
    
        public User getUser() {
            return user;
        }
        public void setUser(User user) {
            this.user = user;
        }
    }

     

     

    2.2.3.2. Sql语句

    SELECT * FROM user WHERE username LIKE ''%%''

    2.2.3.3. Mapper.xml文件

    UserMapper.xml中配置sql,如下图。

     

     

    2.2.3.4. Mapper接口

    UserMapper接口中添加方法,如下图:

     

     

    2.2.3.5. 测试方法

    在UserMapeprTest增加测试方法,如下:

    @Test
    public void testQueryUserByQueryVo() {
        // mybatis和spring整合,整合之后,交给spring管理
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        // 创建Mapper接口的动态代理对象,整合之后,交给spring管理
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        // 使用userMapper执行查询,使用包装对象
        QueryVo queryVo = new QueryVo();
        // 设置user条件
        User user = new User();
        user.setUsername("张");
        // 设置到包装对象中
        queryVo.setUser(user);
    
        // 执行查询
        List<User> list = userMapper.queryUserByQueryVo(queryVo);
        for (User u : list) {
            System.out.println(u);
        }
    
        // mybatis和spring整合,整合之后,交给spring管理
        sqlSession.close();
    }

     

     

    2.2.3.6. 效果

    测试结果如下图:

     

     

     

     

     

    2.3. resultType(输出类型)

    2.3.1. 输出简单类型

    需求:查询用户表数据条数

     

    sqlSELECT count(*) FROM `user`

     

    2.3.1.1. Mapper.xml文件

    UserMapper.xml中配置sql,如下图:

     

     

     

    2.3.1.2. Mapper接口

    UserMapper添加方法,如下图:

     

     

     

    2.3.1.3. 测试方法

    在UserMapeprTest增加测试方法,如下:

    @Test
    public void testQueryUserCount() {
        // mybatis和spring整合,整合之后,交给spring管理
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        // 创建Mapper接口的动态代理对象,整合之后,交给spring管理
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        // 使用userMapper执行查询用户数据条数
        int count = userMapper.queryUserCount();
        System.out.println(count);
    
        // mybatis和spring整合,整合之后,交给spring管理
        sqlSession.close();
    }

     

    2.3.1.4. 效果

    测试结果如下图:

     

     

    注意:输出简单类型必须查询出来的结果集有一条记录,最终将第一个字段的值转换为输出类型。

     

    2.3.2. 输出pojo对象

    参考第一天内容

    2.3.3. 输出pojo列表

    参考第一天内容。

    2.4. resultMap

    resultType可以指定将查询结果映射为pojo,但需要pojo的属性名和sql查询的列名一致方可映射成功。

    如果sql查询字段名和pojo的属性名不一致,可以通过resultMap将字段名和属性名作一个对应关系 ,resultMap实质上还需要将查询结果映射到pojo对象中。

    resultMap可以实现将查询结果映射为复杂类型的pojo,比如在查询结果映射对象中包括pojolist实现一对一查询和一对多查询。

     

    需求:查询订单表order的所有数据

    sqlSELECT id, user_id, number, createtime, note FROM `order`

    2.4.1. 声明pojo对象

    数据库表如下图:

     

     

     

    Order对象:

    public class Order {
        // 订单id
        private int id;
        // 用户id
        private Integer userId;
        // 订单号
        private String number;
        // 订单创建时间
        private Date createtime;
        // 备注
        private String note;
    get/set。。。
    }

     

     

    2.4.2. Mapper.xml文件

    创建OrderMapper.xml配置文件,如下:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!-- namespace:命名空间,用于隔离sql,还有一个很重要的作用,Mapper动态代理开发的时候使用,需要指定Mapper的类路径 -->
    <mapper namespace="cn.itcast.mybatis.mapper.OrderMapper">
        <!-- 查询所有的订单数据 -->
        <select id="queryOrderAll" resultType="order">
            SELECT id, user_id,
            number,
            createtime, note FROM `order`
        </select>
    </mapper>

     

    2.4.3. Mapper接口

    编写接口如下:

     

    public interface OrderMapper {
        /**
         * 查询所有订单
         * 
         * @return
         */
        List<Order> queryOrderAll();
    }public interface OrderMapper {
        /**
         * 查询所有订单
         * 
         * @return
         */
        List<Order> queryOrderAll();
    }

     

    2.4.4. 测试方法

    编写测试方法OrderMapperTest如下:

    public class OrderMapperTest {
        private SqlSessionFactory sqlSessionFactory;
    
        @Before
        public void init() throws Exception {
            InputStream inputStream = Resources.getResourceAsStream("SqlMapConfig.xml");
            this.sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        }
    
        @Test
        public void testQueryAll() {
            // 获取sqlSession
            SqlSession sqlSession = this.sqlSessionFactory.openSession();
            // 获取OrderMapper
            OrderMapper orderMapper = sqlSession.getMapper(OrderMapper.class);
    
            // 执行查询
            List<Order> list = orderMapper.queryOrderAll();
            for (Order order : list) {
                System.out.println(order);
            }
        }
    }

     

     

    2.4.5. 效果

    测试效果如下图:

     

     

    发现userIdnull

    解决方案:使用resultMap

     

    2.4.6. 使用resultMap

    由于上边的mapper.xmlsql查询列(user_id)Order类属性(userId)不一致,所以查询结果不能映射到pojo中。

    需要定义resultMap,把orderResultMapsql查询列(user_id)Order类属性(userId)对应起来

     

    改造OrderMapper.xml,如下:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <!-- namespace:命名空间,用于隔离sql,还有一个很重要的作用,Mapper动态代理开发的时候使用,需要指定Mapper的类路径 -->
    <mapper namespace="cn.itcast.mybatis.mapper.OrderMapper">
    
        <!-- resultMap最终还是要将结果映射到pojo上,type就是指定映射到哪一个pojo -->
        <!-- id:设置ResultMap的id -->
        <resultMap type="order" id="orderResultMap">
            <!-- 定义主键 ,非常重要。如果是多个字段,则定义多个id -->
            <!-- property:主键在pojo中的属性名 -->
            <!-- column:主键在数据库中的列名 -->
            <id property="id" column="id" />
    
            <!-- 定义普通属性 -->
            <result property="userId" column="user_id" />
            <result property="number" column="number" />
            <result property="createtime" column="createtime" />
            <result property="note" column="note" />
        </resultMap>
    
        <!-- 查询所有的订单数据 -->
        <select id="queryOrderAll" resultMap="orderResultMap">
            SELECT id, user_id,
            number,
            createtime, note FROM `order`
        </select>
    
    </mapper>

     

     

    2.4.7. 效果

    只需要修改Mapper.xml就可以了,再次测试结果如下:

     

     

     

    3. 动态sql

    通过mybatis提供的各种标签方法实现动态拼接sql

     

    需求:根据性别和名字查询用户

    查询sql

    SELECT id, username, birthday, sex, address FROM `user` WHERE sex = 1 AND username LIKE ''%%''

    3.1. If标签

    3.1.1. Mapper.xml文件

    UserMapper.xml配置sql,如下:

     

    <!-- 根据条件查询用户 -->
    <select id="queryUserByWhere" parameterType="user" resultType="user">
        SELECT id, username, birthday, sex, address FROM `user`
        WHERE sex = #{sex} AND username LIKE
        ''%${username}%''
    </select>

     

    3.1.2. Mapper接口

    编写Mapper接口,如下图:

     

     

     

    3.1.3. 测试方法

    UserMapperTest添加测试方法,如下:

     

    @Test
    public void testQueryUserByWhere() {
        // mybatis和spring整合,整合之后,交给spring管理
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        // 创建Mapper接口的动态代理对象,整合之后,交给spring管理
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        // 使用userMapper执行根据条件查询用户
        User user = new User();
        user.setSex("1");
        user.setUsername("张");
    
        List<User> list = userMapper.queryUserByWhere(user);
    
        for (User u : list) {
            System.out.println(u);
        }
    
        // mybatis和spring整合,整合之后,交给spring管理
        sqlSession.close();
    }

     

    3.1.4. 效果

    测试效果如下图:

     

     

     

    如果注释掉 user.setSex("1"),测试结果如下图:

     

     

    测试结果二很显然不合理。

    按照之前所学的,要解决这个问题,需要编写多个sql,查询条件越多,需要编写的sql就更多了,显然这样是不靠谱的。

     

    解决方案,使用动态sqlif标签

     

    3.1.5. 使用if标签

    改造UserMapper.xml,如下:

    <!-- 根据条件查询用户 -->
    <select id="queryUserByWhere" parameterType="user" resultType="user">
        SELECT id, username, birthday, sex, address FROM `user`
        WHERE 1=1
        <if test="sex != null and sex != ''''">
            AND sex = #{sex}
        </if>
        <if test="username != null and username != ''''">
            AND username LIKE
            ''%${username}%''
        </if>
    </select>

     

     

    注意字符串类型的数据需要要做不等于空字符串校验。

    3.1.6. 效果

     

     

    如上图所示,测试OK

     

    3.2. Where标签

    上面的sql还有where 1=1 这样的语句,很麻烦

    可以使用where标签进行改造

     

    改造UserMapper.xml,如下

    <!-- 根据条件查询用户 -->
    <select id="queryUserByWhere" parameterType="user" resultType="user">
        SELECT id, username, birthday, sex, address FROM `user`
    <!-- where标签可以自动添加where,同时处理sql语句中第一个and关键字 -->
        <where>
            <if test="sex != null">
                AND sex = #{sex}
            </if>
            <if test="username != null and username != ''''">
                AND username LIKE
                ''%${username}%''
            </if>
        </where>
    </select>

     

     

    3.2.1. 效果

    测试效果如下图:

     

     

     

    3.3. Sql片段

    Sql中可将重复的sql提取出来,使用时用include引用即可,最终达到sql重用的目的。

     

    把上面例子中的id, username, birthday, sex, address提取出来,作为sql片段,如下:

    <!-- 根据条件查询用户 -->
    <select id="queryUserByWhere" parameterType="user" resultType="user">
        <!-- SELECT id, username, birthday, sex, address FROM `user` -->
        <!-- 使用include标签加载sql片段;refid是sql片段id -->
        SELECT <include refid="userFields" /> FROM `user`
        <!-- where标签可以自动添加where关键字,同时处理sql语句中第一个and关键字 -->
        <where>
            <if test="sex != null">
                AND sex = #{sex}
            </if>
            <if test="username != null and username != ''''">
                AND username LIKE
                ''%${username}%''
            </if>
        </where>
    </select>
    
    <!-- 声明sql片段 -->
    <sql id="userFields">
        id, username, birthday, sex, address
    </sql>

     

     

    如果要使用别的Mapper.xml配置的sql片段,可以在refid前面加上对应的Mapper.xmlnamespace

    例如下图

     

     

    3.4. foreach标签

    sql传递数组或Listmybatis使用foreach解析,如下:

     

    根据多个id查询用户信息

    查询sql

    SELECT * FROM user WHERE id IN (1,10,24)

     

    3.4.1. 改造QueryVo

    如下图在pojo中定义list属性ids存储多个用户id,并添加getter/setter方法

     

     

     

    3.4.2. Mapper.xml文件

    UserMapper.xml添加sql,如下:

    <!-- 根据ids查询用户 -->
    <select id="queryUserByIds" parameterType="queryVo" resultType="user">
        SELECT * FROM `user`
        <where>
            <!-- foreach标签,进行遍历 -->
            <!-- collection:遍历的集合,这里是QueryVo的ids属性 -->
            <!-- item:遍历的项目,可以随便写,,但是和后面的#{}里面要一致 -->
            <!-- open:在前面添加的sql片段 -->
            <!-- close:在结尾处添加的sql片段 -->
            <!-- separator:指定遍历的元素之间使用的分隔符 -->
            <foreach collection="ids" item="item" open="id IN (" close=")"
                separator=",">
                #{item}
            </foreach>
        </where>
    </select>

     

     

    测试方法如下图:

    @Test
    public void testQueryUserByIds() {
        // mybatis和spring整合,整合之后,交给spring管理
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        // 创建Mapper接口的动态代理对象,整合之后,交给spring管理
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        // 使用userMapper执行根据条件查询用户
        QueryVo queryVo = new QueryVo();
        List<Integer> ids = new ArrayList<>();
        ids.add(1);
        ids.add(10);
        ids.add(24);
        queryVo.setIds(ids);
    
        List<User> list = userMapper.queryUserByIds(queryVo);
    
        for (User u : list) {
            System.out.println(u);
        }
    
        // mybatis和spring整合,整合之后,交给spring管理
        sqlSession.close();
    }

     

    3.4.3. 效果

    测试效果如下图:

     

     

    其他实现:

     

    底层原理:传递进去的user并没有被引用,而是重新创建一个mapuser里面的值存在map里面,传入如果是数组那就会放在array中,如果是List就会放在list中。

     

     

     

    4. 关联查询

    4.1. 商品订单数据模型

     

     

     

    4.2. 一对一查询

    需求:查询所有订单信息,关联查询下单用户信息。

     

    注意:因为一个订单信息只会是一个人下的订单,所以从查询订单信息出发关联查询用户信息为一对一查询。如果从用户信息出发查询用户下的订单信息则为一对多查询,因为一个用户可以下多个订单。

     

    sql语句:

    SELECT
        o.id,
        o.user_id userId,
        o.number,
        o.createtime,
        o.note,
        u.username,
        u.address
    FROM
        `orders` o
    LEFT JOIN `user` u ON o.user_id = u.id

     

     

    4.2.1. 方法一:使用resultType

    使用resultType,改造订单pojo类,此pojo类中包括了订单信息和用户信息

    这样返回对象的时候,mybatis自动把用户信息也注入进来了

    4.2.1.1. 改造pojo

    OrderUser类继承Order类后OrderUser类包括了Order类的所有字段,只需要定义用户的信息字段即可,如下图:

     

     

    4.2.1.2. Mapper.xml

    UserMapper.xml添加sql,如下

    <!-- 查询订单,同时包含用户数据 -->
    <select id="queryOrderUser" resultType="orderUser">
        SELECT
        o.id,
        o.user_id
        userId,
        o.number,
        o.createtime,
        o.note,
        u.username,
        u.address
        FROM
        `order` o
        LEFT JOIN `user` u ON o.user_id = u.id
    </select>

     

    4.2.1.3. Mapper接口

    UserMapper接口添加方法,如下图:

     

     

    4.2.1.4. 测试方法:

    UserMapperTest添加测试方法,如下:

    @Test
    public void testQueryOrderUser() {
        // mybatis和spring整合,整合之后,交给spring管理
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        // 创建Mapper接口的动态代理对象,整合之后,交给spring管理
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        // 使用userMapper执行根据条件查询用户
        List<OrderUser> list = userMapper.queryOrderUser();
    
        for (OrderUser ou : list) {
            System.out.println(ou);
        }
    
        // mybatis和spring整合,整合之后,交给spring管理
        sqlSession.close();
    }

     

    4.2.1.5. 效果

    测试结果如下图:

     

     

    4.2.1.6. 小结

    定义专门的pojo类作为输出类型,其中定义了sql查询结果集所有的字段。此方法较为简单,企业中使用普遍。

     

    4.2.2. 方法二:使用resultMap

    使用resultMap,定义专门的resultMap用于映射一对一查询结果。

     

    4.2.2.1. 改造pojo

    Order类中加入User属性,user属性中用于存储关联查询的用户信息,因为订单关联查询用户是一对一关系,所以这里使用单个User对象存储关联查询的用户信息。

    改造Order如下图:

     

     

     

    4.2.2.2. Mapper.xml

    这里resultMap指定orderUserResultMap,如下:

    <resultMap type="order" id="orderUserResultMap">
        <id property="id" column="id" />
        <result property="userId" column="user_id" />
        <result property="number" column="number" />
        <result property="createtime" column="createtime" />
        <result property="note" column="note" />
    
        <!-- association :配置一对一属性 -->
        <!-- property:order里面的User属性名 -->
        <!-- javaType:属性类型 -->
        <association property="user" javaType="user">
            <!-- id:声明主键,表示user_id是关联查询对象的唯一标识-->
            <id property="id" column="user_id" />
            <result property="username" column="username" />
            <result property="address" column="address" />
        </association>
    
    </resultMap>
    
    <!-- 一对一关联,查询订单,订单内部包含用户属性 -->
    <select id="queryOrderUserResultMap" resultMap="orderUserResultMap">
        SELECT
        o.id,
        o.user_id,
        o.number,
        o.createtime,
        o.note,
        u.username,
        u.address
        FROM
        `order` o
        LEFT JOIN `user` u ON o.user_id = u.id
    </select>

     

    4.2.2.3. Mapper接口

    编写UserMapper如下图:

     

     

     

    4.2.2.4. 测试方法

    UserMapperTest增加测试方法,如下:

    @Test
    public void testQueryOrderUserResultMap() {
        // mybatis和spring整合,整合之后,交给spring管理
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        // 创建Mapper接口的动态代理对象,整合之后,交给spring管理
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        // 使用userMapper执行根据条件查询用户
        List<Order> list = userMapper.queryOrderUserResultMap();
    
        for (Order o : list) {
            System.out.println(o);
        }
    
        // mybatis和spring整合,整合之后,交给spring管理
        sqlSession.close();
    }

     

    4.2.2.5. 效果

    测试效果如下图:

     

     

     

    4.3. 一对多查询

    案例:查询所有用户信息及用户关联的订单信息。

    用户信息和订单信息为一对多关系。

     

    sql语句:

    SELECT
        u.id,
        u.username,
        u.birthday,
        u.sex,
        u.address,
        o.id oid,
        o.number,
        o.createtime,
        o.note
    FROM
        `user` u
    LEFT JOIN `order` o ON u.id = o.user_id

     

    4.3.1. 修改pojo

    User类中加入List<Order> orders属性,如下图:

     

     

     

    4.3.2. Mapper.xml

    UserMapper.xml添加sql,如下:

    <resultMap type="user" id="userOrderResultMap">
        <id property="id" column="id" />
        <result property="username" column="username" />
        <result property="birthday" column="birthday" />
        <result property="sex" column="sex" />
        <result property="address" column="address" />
    
        <!-- 配置一对多的关系 -->
        <collection property="orders" javaType="list" ofType="order">
            <!-- 配置主键,是关联Order的唯一标识 -->
            <id property="id" column="oid" />
            <result property="number" column="number" />
            <result property="createtime" column="createtime" />
            <result property="note" column="note" />
        </collection>
    </resultMap>
    
    <!-- 一对多关联,查询订单同时查询该用户下的订单 -->
    <select id="queryUserOrder" resultMap="userOrderResultMap">
        SELECT
        u.id,
        u.username,
        u.birthday,
        u.sex,
        u.address,
        o.id oid,
        o.number,
        o.createtime,
        o.note
        FROM
        `user` u
        LEFT JOIN `order` o ON u.id = o.user_id
    </select>

     

    4.3.3. Mapper接口

    编写UserMapper接口,如下图:

     

     

     

    4.3.4. 测试方法

    UserMapperTest增加测试方法,如下

    @Test
    public void testQueryUserOrder() {
        // mybatis和spring整合,整合之后,交给spring管理
        SqlSession sqlSession = this.sqlSessionFactory.openSession();
        // 创建Mapper接口的动态代理对象,整合之后,交给spring管理
        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
    
        // 使用userMapper执行根据条件查询用户
        List<User> list = userMapper.queryUserOrder();
    
        for (User u : list) {
            System.out.println(u);
        }
    
        // mybatis和spring整合,整合之后,交给spring管理
        sqlSession.close();
    }

     

    4.3.5. 效果

    测试效果如下图:

     

     

     

     

     

    5. Mybatis整合spring

    5.1. 整合思路

    1、SqlSessionFactory对象应该放到spring容器中作为单例存在。

    2、传统dao的开发方式中,应该从spring容器中获得sqlsession对象。

    3、Mapper代理形式中,应该从spring容器中直接获得mapper的代理对象。

    4、数据库的连接以及数据库连接池事务管理都交给spring容器来完成。

    5.2. 整合需要的jar

    1、springjar

    2、Mybatisjar

    3、Spring+mybatis的整合包。

    4、Mysql的数据库驱动jar包。

    5、数据库连接池的jar包。

    jar包位置如下所示:

     

    5.3. 整合的步骤

    5.3.1. 创建工程

    如下图创建一个java工程:

     

     

    5.3.2. 导入jar

    前面提到的jar包需要导入,如下图:

     

     

     

    5.3.3. 加入配置文件

    1. mybatisSpring的配置文件
    2. 的配置文件sqlmapConfig.xml

    a) 数据库连接及连接池

    b) 事务管理(暂时可以不配置)

    c) sqlsessionFactory对象,配置到spring容器中

    d) mapeer代理对象或者是dao实现类配置到spring容器中。

     

    创建资源文件夹config拷贝加入配置文件,如下图

     

     

     

    5.3.3.1. SqlMapConfig.xml

    配置文件是SqlMapConfig.xml,如下:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <!-- 设置别名 -->
        <typeAliases>
            <!-- 2. 指定扫描包,会把包内所有的类都设置别名,别名的名称就是类名,大小写不敏感 -->
            <package name="cn.itcast.mybatis.pojo" />
        </typeAliases>
    
    </configuration>

     

     

    5.3.3.2. applicationContext.xml

    SqlSessionFactoryBean属于mybatis-spring这个jar

    对于spring来说,mybatis是另外一个架构,需要整合jar包。

     

    在项目中加入mybatis-spring-1.2.2.jar的源码,如下图

     

     

     

     

    效果,如下图所示,图标变化,表示源码加载成功:

     

     

    整合Mybatis需要的是SqlSessionFactoryBean,位置如下图:

     

     

     

    applicationContext.xml,配置内容如下

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:context="http://www.springframework.org/schema/context" xmlns:p="http://www.springframework.org/schema/p"
        xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd">
    
       <!-- 加载配置文件 -->
       <context:property-placeholder location="classpath:db.properties" />
    
        <!-- 数据库连接池 -->
        <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
            destroy-method="close">
            <property name="driverClassName" value="${jdbc.driver}" />
            <property name="url" value="${jdbc.url}" />
            <property name="username" value="${jdbc.username}" />
            <property name="password" value="${jdbc.password}" />
            <property name="maxActive" value="10" />
            <property name="maxIdle" value="5" />
        </bean>
    
        <!-- 配置SqlSessionFactory -->
        <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
            <!-- 配置mybatis核心配置文件 -->
            <property name="configLocation" value="classpath:SqlMapConfig.xml" />
            <!-- 配置数据源 -->
            <property name="dataSource" ref="dataSource" />
        </bean>
    </beans>

     

    5.3.3.3. db.properties

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://localhost:3306/mybatis?characterEncoding=utf-8
    jdbc.username=root
    jdbc.password=root

    5.3.3.4. log4j.properties

    # Global logging configuration
    log4j.rootLogger=DEBUG, stdout
    # Console output...
    log4j.appender.stdout=org.apache.log4j.ConsoleAppender
    log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
    log4j.appender.stdout.layout.ConversionPattern=%5p [%t] - %m%n

    5.3.3.5. 效果:

    加入的配置文件最终效果如下:

     

     

    5.4. Dao的开发

    两种dao的实现方式:

    1、原始dao的开发方式

    2、使用Mapper代理形式开发方式

    a) 直接配置Mapper代理

    b) 使用扫描包配置Mapper代理

     

    需求:

    1. 实现根据用户id查询
    2. 实现根据用户名模糊查询
    3. 添加用户

     

    5.4.1. 创建pojo

    public class User {
        private int id;
        private String username;// 用户姓名
        private String sex;// 性别
        private Date birthday;// 生日
        private String address;// 地址
    
    get/set。。。
    }

     

     

    5.4.2. 传统dao的开发方式

    原始的DAO开发接口+实现类来完成。

    需要dao实现类需要继承SqlsessionDaoSupport

     

    5.4.2.1. 实现Mapper.xml

    编写User.xml配置文件,如下:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="test">
        <!-- 根据用户id查询 -->
        <select id="queryUserById" parameterType="int" resultType="user">
            select * from user where id = #{id}
        </select>
    
        <!-- 根据用户名模糊查询用户 -->
        <select id="queryUserByUsername" parameterType="string"
            resultType="user">
            select * from user where username like ''%${value}%''
        </select>
    
        <!-- 添加用户 -->
        <insert id="saveUser" parameterType="user">
            <selectKey keyProperty="id" keyColumn="id" order="AFTER"
                resultType="int">
                select last_insert_id()
            </selectKey>
            insert into user
            (username,birthday,sex,address)
            values
            (#{username},#{birthday},#{sex},#{address})
        </insert>
    
    </mapper>

     

    5.4.2.2. 加载Mapper.xml

    SqlMapConfig如下图进行配置:

     

     

     

    5.4.2.3. 实现UserDao接口

    public interface UserDao {
        /**
         * 根据id查询用户
         * 
         * @param id
         * @return
         */
        User queryUserById(int id);
    
        /**
         * 根据用户名模糊查询用户列表
         * 
         * @param username
         * @return
         */
        List<User> queryUserByUsername(String username);
    
        /**
         * 保存
         * 
         * @param user
         */
        void saveUser(User user);
    
    }

     

     

     

    5.4.2.4. 实现UserDaoImpl实现类

    编写DAO实现类,实现类必须集成SqlSessionDaoSupport

    SqlSessionDaoSupport提供getSqlSession()方法来获取SqlSession

    public class UserDaoImpl extends SqlSessionDaoSupport implements UserDao {
        @Override
        public User queryUserById(int id) {
            // 获取SqlSession
            SqlSession sqlSession = super.getSqlSession();
    
            // 使用SqlSession执行操作
            User user = sqlSession.selectOne("queryUserById", id);
    
            // 不要关闭sqlSession
    
            return user;
        }
    
        @Override
        public List<User> queryUserByUsername(String username) {
            // 获取SqlSession
            SqlSession sqlSession = super.getSqlSession();
    
            // 使用SqlSession执行操作
            List<User> list = sqlSession.selectList("queryUserByUsername", username);
    
            // 不要关闭sqlSession
    
            return list;
        }
    
        @Override
        public void saveUser(User user) {
            // 获取SqlSession
            SqlSession sqlSession = super.getSqlSession();
    
            // 使用SqlSession执行操作
            sqlSession.insert("saveUser", user);
    
            // 不用提交,事务由spring进行管理
            // 不要关闭sqlSession
        }
    }

     

     

    5.4.2.5. 配置dao

    dao实现类配置到spring容器中,如下图

     

     

     

    5.4.2.6. 测试方法

    创建测试方法,可以直接创建测试Junit用例。

    如下图所示进行创建。

     

     

    编写测试方法如下:

     

    public class UserDaoTest {
        private ApplicationContext context;
    
        @Before
        public void setUp() throws Exception {
            this.context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        }
    
        @Test
        public void testQueryUserById() {
            // 获取userDao
            UserDao userDao = this.context.getBean(UserDao.class);
    
            User user = userDao.queryUserById(1);
            System.out.println(user);
        }
    
        @Test
        public void testQueryUserByUsername() {
            // 获取userDao
            UserDao userDao = this.context.getBean(UserDao.class);
    
            List<User> list = userDao.queryUserByUsername("张");
            for (User user : list) {
                System.out.println(user);
            }
        }
    
        @Test
        public void testSaveUser() {
            // 获取userDao
            UserDao userDao = this.context.getBean(UserDao.class);
    
            User user = new User();
            user.setUsername("曹操");
            user.setSex("1");
            user.setBirthday(new Date());
            user.setAddress("三国");
            userDao.saveUser(user);
            System.out.println(user);
        }
    }

     

    5.4.3. Mapper代理形式开发dao

    5.4.3.1. 实现Mapper.xml

    编写UserMapper.xml配置文件,如下:

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="cn.itcast.mybatis.mapper.UserMapper">
        <!-- 根据用户id查询 -->
        <select id="queryUserById" parameterType="int" resultType="user">
            select * from user where id = #{id}
        </select>
    
        <!-- 根据用户名模糊查询用户 -->
        <select id="queryUserByUsername" parameterType="string"
            resultType="user">
            select * from user where username like ''%${value}%''
        </select>
    
        <!-- 添加用户 -->
        <insert id="saveUser" parameterType="user">
            <selectKey keyProperty="id" keyColumn="id" order="AFTER"
                resultType="int">
                select last_insert_id()
            </selectKey>
            insert into user
            (username,birthday,sex,address) values
            (#{username},#{birthday},#{sex},#{address})
        </insert>
    </mapper>

     

    5.4.3.2. 实现UserMapper接口

    public interface UserMapper {
        /**
         * 根据用户id查询
         * 
         * @param id
         * @return
         */
        User queryUserById(int id);
    
        /**
         * 根据用户名模糊查询用户
         * 
         * @param username
         * @return
         */
        List<User> queryUserByUsername(String username);
    
        /**
         * 添加用户
         * 
         * @param user
         */
        void saveUser(User user);
    }

     

    5.4.3.3. 方式一:配置mapper代理

    applicationContext.xml添加配置

    MapperFactoryBean也是属于mybatis-spring整合包

    <!-- Mapper代理的方式开发方式一,配置Mapper代理对象 -->
    <bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
        <!-- 配置Mapper接口 -->
        <property name="mapperInterface" value="cn.itcast.mybatis.mapper.UserMapper" />
        <!-- 配置sqlSessionFactory -->
        <property name="sqlSessionFactory" ref="sqlSessionFactory" />
    </bean>

     

    5.4.3.4. 测试方法

    public class UserMapperTest {
        private ApplicationContext context;
    
        @Before
        public void setUp() throws Exception {
            this.context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        }
    
        @Test
        public void testQueryUserById() {
            // 获取Mapper
            UserMapper userMapper = this.context.getBean(UserMapper.class);
    
            User user = userMapper.queryUserById(1);
            System.out.println(user);
        }
    
        @Test
        public void testQueryUserByUsername() {
            // 获取Mapper
            UserMapper userMapper = this.context.getBean(UserMapper.class);
    
            List<User> list = userMapper.queryUserByUsername("张");
    
            for (User user : list) {
                System.out.println(user);
            }
        }
        @Test
        public void testSaveUser() {
            // 获取Mapper
            UserMapper userMapper = this.context.getBean(UserMapper.class);
    
            User user = new User();
            user.setUsername("曹操");
            user.setSex("1");
            user.setBirthday(new Date());
            user.setAddress("三国");
    
            userMapper.saveUser(user);
            System.out.println(user);
        }
    }

     

     

    5.4.3.5. 方式二:扫描包形式配置mapper

    <!-- Mapper代理的方式开发方式二,扫描包方式配置代理 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 配置Mapper接口 -->
        <property name="basePackage" value="cn.itcast.mybatis.mapper" />
    </bean>

     

    每个mapper代理对象的id就是类名,首字母小写

    6. Mybatis逆向工程

    使用官方网站的Mapper自动生成工具mybatis-generator-core-1.3.2来生成po类和Mapper映射文件

    6.1. 导入逆向工程

    使用课前资料已有逆向工程,如下图:

     

     

     

    6.1.1. 复制逆向工程到工作空间中

    复制的效果如下图:

     

     

     

    6.1.2. 导入逆向工程到eclipse

    如下图方式进行导入:

     

     

     

    6.2. 修改配置文件

    generatorConfig.xml中配置Mapper生成的详细信息,如下图:

     

     

    注意修改以下几点:

    1. 修改要生成的数据库表
    2. pojo文件所在包路径
    3. Mapper所在的包路径

     

    配置文件如下:

     

    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE generatorConfiguration
      PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
      "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">
    
    <generatorConfiguration>
        <context id="testTables" targetRuntime="MyBatis3">
            <commentGenerator>
                <!-- 是否去除自动生成的注释 true:是 : false:否 -->
                <property name="suppressAllComments" value="true" />
            </commentGenerator>
            <!--数据库连接的信息:驱动类、连接地址、用户名、密码 -->
            <jdbcConnection driverClass="com.mysql.jdbc.Driver"
                connectionURL="jdbc:mysql://localhost:3306/mybatis" userId="root" password="root">
            </jdbcConnection>
            <!-- <jdbcConnection driverconnectionURL="jdbc:oracle:thin:@127.0.0.1:1521:yycg" 
                userId="yycg" password="yycg"> </jdbcConnection> -->
    
            <!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL 
                和 NUMERIC 类型解析为java.math.BigDecimal -->
            <javaTypeResolver>
                <property name="forceBigDecimals" value="false" />
            </javaTypeResolver>
    
            <!-- targetProject:生成PO类的位置 -->
            <javaModelGenerator targetPackage="cn.itcast.ssm.po"
                targetProject=".\src">
                <!-- enableSubPackages:是否让schema作为包的后缀 -->
                <property name="enableSubPackages" value="false" />
                <!-- 从数据库返回的值被清理前后的空格 -->
                <property name="trimStrings" value="true" />
            </javaModelGenerator>
            <!-- targetProject:mapper映射文件生成的位置 -->
            <sqlMapGenerator targetPackage="cn.itcast.ssm.mapper"
                targetProject=".\src">
                <!-- enableSubPackages:是否让schema作为包的后缀 -->
                <property name="enableSubPackages" value="false" />
            </sqlMapGenerator>
            <!-- targetPackage:mapper接口生成的位置 -->
            <javaClientGenerator type="XMLMAPPER"
                targetPackage="cn.itcast.ssm.mapper" targetProject=".\src">
                <!-- enableSubPackages:是否让schema作为包的后缀 -->
                <property name="enableSubPackages" value="false" />
            </javaClientGenerator>
            <!-- 指定数据库表 -->
            <table schema="" tableName="user"></table>
            <table schema="" tableName="order"></table>
        </context>
    </generatorConfiguration>

     

    6.3. 生成逆向工程代码

    找到下图所示的java文件,执行工程main主函数,

     

     

     

     

    刷新工程,发现代码生成,如下图:

     

     

    6.4. 测试逆向工程代码

    1. 复制生成代码到mybatis-spring工程,如下图

     

     

     

    2. 修改spring配置文件

    applicationContext.xml修改

     

    <!-- Mapper代理的方式开发,扫描包方式配置代理 -->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <!-- 配置Mapper接口,如果需要加载多个包,直接写进来,中间用,分隔 -->
        <!-- <property name="basePackage" value="cn.itcast.mybatis.mapper" /> -->
        <property name="basePackage" value="cn.itcast.ssm.mapper" />
    </bean>

     

     

    3. 编写测试方法:

     

    public class UserMapperTest {
        private ApplicationContext context;
    
        @Before
        public void setUp() throws Exception {
            this.context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        }
    
        @Test
        public void testInsert() {
            // 获取Mapper
            UserMapper userMapper = this.context.getBean(UserMapper.class);
    
            User user = new User();
            user.setUsername("曹操");
            user.setSex("1");
            user.setBirthday(new Date());
            user.setAddress("三国");
    
            userMapper.insert(user);
        }
    
        @Test
        public void testSelectByExample() {
            // 获取Mapper
            UserMapper userMapper = this.context.getBean(UserMapper.class);
    
            // 创建User对象扩展类,用户设置查询条件
            UserExample example = new UserExample();
            example.createCriteria().andUsernameLike("%张%");
    
            // 查询数据
            List<User> list = userMapper.selectByExample(example);
    
            System.out.println(list.size());
        }
    
        @Test
        public void testSelectByPrimaryKey() {
            // 获取Mapper
            UserMapper userMapper = this.context.getBean(UserMapper.class);
    
            User user = userMapper.selectByPrimaryKey(1);
            System.out.println(user);
        }
    }

     

    注意:

      1. 逆向工程生成的代码只能做单表查询
      2. 不能在生成的代码上进行扩展,因为如果数据库变更,需要重新使用逆向工程生成代码,原来编写的代码就被覆盖了。
      3. 一张表会生成4个文件

     

    mybatis 学习笔记(二):mybatis SQL注入问题

    mybatis 学习笔记(二):mybatis SQL注入问题

    mybatis 学习笔记(二):mybatis SQL注入问题

    SQL 注入攻击

    首先了解下概念,什么叫SQL 注入:

    SQL注入攻击,简称SQL攻击或注入攻击,是发生于应用程序之数据库层的安全漏洞。简而言之,是在输入的字符串之中注入SQL指令,在设计不良的程序当中忽略了检查,那么这些注入进去的指令就会被数据库服务器误认为是正常的SQL指令而运行,因此遭到破坏或是入侵。

    最常见的就是我们在应用程序中使用字符串联结方式组合 SQL 指令,有心之人就会写一些特殊的符号,恶意篡改原本的 SQL 语法的作用,达到注入攻击的目的。

    举个栗子:

    比如验证用户登录需要 username 和 password,编写的 SQL 语句如下:

    select * from user where (name = ''"+ username +"'') and (pw = ''"+ password +"'');
    

    username 和 password 字段被恶意填入

    username = "1'' OR ''1''=''1";
    

    password = "1'' OR ''1''=''1";
    

    将导致原本的 SQL 字符串被填为:

    select * from user where (name = ''1'' or ''1''=''1'') and (pw = ''1'' or ''1''=''1'');
    

    实际上运行的 SQL 语句将变成:

    select * from user;
    

    也就是不再需要 username 和 password 账密即达到登录的目的,结果不言而喻。

    mybatis 解决 SQL 注入问题

    我们使用 mybatis 编写 SQL 语句时,难免会使用模糊查询的方法,mybatis 提供了两种方式 #{}${}

    举个栗子:

    1 查询数据库 sample 表 user 中的记录,我们故意使用特殊符号,看能否引起 SQL 注入。使用 mybatis 在 mapper.xml 配置文件中编写 SQL 语句,我们先采用拼接字符串形式,看看结果如何:

     <select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User">
            <!-- 拼接 MySQL,引起 SQL 注入 -->
            SELECT * FROM user WHERE username LIKE ''%${value}%''
        </select>
    

    注意在配置文件中编写 SQL 语句时,后边不需要加分号。

    调用配置文件,编写测试文件,查询数据库内容,采用特殊符号,引起 SQL 注入:

        @Test
        public void testFindUserByName() throws Exception{
    
            SqlSession sqlSession=sqlSessionFactory.openSession();
    
            //创建UserMapper代理对象
            UserMapper userMapper=sqlSession.getMapper(UserMapper.class);
    
            //调用userMapper的方法
            List<User> list=userMapper.findUserByName("'' or ''1''=''1");
    
            sqlSession.close();
    
            System.out.println(list);
        }
    }
    

    运行结果如下图所示:

    可以看到执行语句其实变为了

    select * from user
    

    将user 表中的全部记录打印出来了。发生了 SQL 注入。

    2 如果将配置文件中的 SQL 语句改成 #{} 形式,可避免 SQL 注入。

     <select id="findUserByName" parameterType="java.lang.String" resultType="cn.itcast.mybatis.po.User">
            <!-- 使用 SQL concat 语句,拼接字符串,防止 SQL 注入 -->
            SELECT * FROM USER WHERE username LIKE CONCAT(''%'',#{value},''%'' )
        </select>
    

    再次运行测试程序,控制台输出如下:

    可以看到程序中参数部分用 ? 替代了,很好地解决了 SQL 语句的问题,防止了 SQL 注入。查询结果将为空。

    关于Mybatis系列全解八:Mybatis的9大动态SQL标签你知道几个?提前致女神!的问题我们已经讲解完毕,感谢您的阅读,如果还想了解更多关于java web(七): mybatis的动态sql和mybatis generator自动生成pojo类和映射文件、JavaEE_Mybatis_SpringMVC__Mybatis_lesson8_Mybatis的动态sql、JAVAEE——Mybatis加强:输入和输出映射、动态sql、关联查询、Mybatis整合spring、Mybatis逆向工程、mybatis 学习笔记(二):mybatis SQL注入问题等相关内容,可以在本站寻找。

    本文标签:

    上一篇mybatis缓存(了解)(mybatis 缓存)

    下一篇MyBatis查询:一对多映射关系(mybatis一对多映射原理)