飞道的博客

MyBatis 动态 SQL 详解(以后写 SQL 爽多了)

504人阅读  评论(0)

动态 SQL 是 MyBatis 的强大特性之一。如果你使用过 JDBC 或其它类似的框架,你应该能理解根据不同条件拼接 SQL 语句有多痛苦,例如拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号。利用动态 SQL,可以彻底摆脱这种痛苦。

01. 环境搭建

在正式开始学习之前,我们先搭建一下此次项目的环境,首先创建数据库和数据表:


   
  1. CREATE DATABASE mybatis;
  2. CREATE TABLE tbl_employee(
  3.  id  INT( 11) PRIMARY KEY AUTO_INCREMENT,
  4.  last_name VARCHAR( 255),
  5.  gender CHAR( 1),
  6.  email VARCHAR( 255)
  7. );
  8. INSERT INTO tbl_employee VALUES( 'tom', 0, 'tom@qq.com');

接下来我们创建一个maven项目,引入MyBatis的依赖:


   
  1. <!-- mybatis -->
  2. <dependency>
  3.   <groupId>org.mybatis</groupId>
  4.   <artifactId>mybatis</artifactId>
  5.   <version> 3.4 .1</version>
  6. </dependency>
  7. <!-- 数据库驱动 -->
  8. <dependency>
  9.   <groupId>mysql</groupId>
  10.   <artifactId>mysql-connector-java</artifactId>
  11.   <version> 5.1 .37</version>
  12. </dependency>

接着创建数据表对应的Bean类:


   
  1. @Data
  2. @ToString
  3. public class Employee {
  4.      private  Integer id;
  5.      private  String lastName;
  6.      private char gender;
  7.      private  String email;
  8. }

在类路径下编写一个XML文件(mybatis-config.xml)来完成SqlSessionFactory的构建:


   
  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.     <environments  default= "development">
  7.         <environment id= "development">
  8.             <transactionManager type= "JDBC"/>
  9.             <!-- 配置数据源 -->
  10.             <dataSource type= "POOLED">
  11.                 <property name= "driver" value= "com.mysql.jdbc.Driver"/>
  12.                 <property name= "url" value= "jdbc:mysql:///mybatis"/>
  13.                 <property name= "username" value= "root"/>
  14.                 <property name= "password" value= "123456"/>
  15.             </dataSource>
  16.         </environment>
  17.     </environments>
  18.     <!-- 注册Mapper映射文件 -->
  19.     <mappers>
  20.         <mapper resource= "EmployeeMapper.xml"/>
  21.     </mappers>
  22. </configuration>

最后创建 Mapper 接口和接口对应的映射文件。

02. if

先来看看第一个动态SQL标签——if,通过if标签,我们能够根据我们传递的参数进行动态查询,比如:根据id查询员工信息,若是携带了哪个参数sql的条件就要带上这个参数对应的字段的值。


   
  1. <select id= "getEmpsByConditionIf" resultType= "com.wwj.mybatis.bean.Employee">
  2.   select *
  3.    from tbl_employee
  4.   where
  5.   < if test= "id != null">
  6.     id= #{id}
  7.   </ if>
  8.   < if test= "lastName != null and lastName != ''">
  9.      and last_name like #{lastName}
  10.   </ if>
  11.   < if test= "email != null and email.trim() != ''">
  12.      and email = #{email}
  13.   </ if>
  14.   < if test= "gender == 0 or gender == 1">
  15.      and gender = #{gender}
  16.   </ if>
  17. </select>

测试代码:


   
  1. EmployeeDynamicSqlMapper mapper = sqlSession.getMapper(EmployeeDynamicSqlMapper. class);
  2. Employee employee = new Employee( 1, "%c%", null, "jack@qq.com");
  3. List<Employee> emps = mapper.getEmpsByConditionIf(employee);
  4. for (Employee emp : emps) {
  5.     System.out.println(emp);
  6. }

我们来看看它生成的sql:


   
  1. ==> Preparing: select * from tbl_employee where id = ? and last_name like ? and email = ?
  2. ==> Parameters:  1( Integer), %c%( String), jack@qq.com( String)
  3. <== Columns: id, last_name, gender, email, d_id
  4. <== Row:  1, jack, 1, jack@qq.com, 1
  5. <== Total:  1
  6. Employee(id= 1, lastName= null, gender= 1, email=jack@qq.com)

然而这样的代码是有问题的,如果我们的条件是这样的:

Employee employee = new Employee(null, "%c%", null, "jack@qq.com");

来看看生成的sql:


   
  1. ==>  Preparing: select * from tbl_employee where and last_name like ? and email = ?
  2. ==> Parameters: %c%( String), jack@qq.com( String)

因为id为null,所以后面的and被直接拼在了where的后面,此时sql语法就会出错,为此我们需要使用另一个标签——where,它能够完美地解决这一问题。

03. where


   
  1. <select id= "getEmpsByConditionIf" resultType= "com.wwj.mybatis.bean.Employee">
  2.   select *
  3.    from tbl_employee
  4.   <where>
  5.     < if test= "id != null">
  6.       id = #{id}
  7.     </ if>
  8.     < if test= "lastName != null and lastName != ''">
  9.        and last_name like #{lastName}
  10.     </ if>
  11.     < if test= "email != null and email.trim() != ''">
  12.        and email = #{email}
  13.     </ if>
  14.     < if test= "gender == 0 or gender == 1">
  15.        and gender = #{gender}
  16.     </ if>
  17.   </where>
  18. </select>

只需将判断条件全部放在where标签内,MyBatis就会自动将where标签拼接的sql中多出来的and和or删掉,然而where标签依然有弊端,比如这样拼接sql:


   
  1. <select id= "getEmpsByConditionIf" resultType= "com.wwj.mybatis.bean.Employee">
  2.   select *
  3.    from tbl_employee
  4.   <where>
  5.     < if test= "id != null">
  6.       id = #{id} and
  7.     </ if>
  8.     < if test= "lastName != null and lastName != ''">
  9.       last_name like #{lastName} and
  10.     </ if>
  11.     < if test= "email != null and email.trim() != ''">
  12.       email = #{email} and
  13.     </ if>
  14.     < if test= "gender == 0 or gender == 1">
  15.       gender = #{gender}
  16.     </ if>
  17.   </where>
  18. </select>

并且传递的参数条件是这种情况:

Employee employee = new Employee(null, "%c%", null, null);

此时last_name属性后面跟着一个and,但是接下来的属性都是null,所以得到了这样的一条sql:


   
  1. ==>  Preparing: select * from tbl_employee WHERE last_name like ? and 
  2. ==> Parameters: %c%( String)

可前面我们已经说到,where标签会自动去除多余的and和or呀,为什么还是报错呢?原来,where标签只能去除属性前多余的and和or,至于属性后的,它就无能为力了,所以要么就将and和or都写在属性的前面,要么就使用trim标签来解决这一问题。

04. trim


   
  1. <select id= "getEmpsByConditionIf" resultType= "com.wwj.mybatis.bean.Employee">
  2.   select *
  3.    from tbl_employee
  4.   <trim prefix= "where" suffixOverrides= "and">
  5.     < if test= "id != null">
  6.       id = #{id} and
  7.     </ if>
  8.     < if test= "lastName != null and lastName != ''">
  9.       last_name like #{lastName} and
  10.     </ if>
  11.     < if test= "email != null and email.trim() != ''">
  12.       email = #{email} and
  13.     </ if>
  14.     < if test= "gender == 0 or gender == 1">
  15.       gender = #{gender}
  16.     </ if>
  17.   </trim>
  18. </select>

trim标签中共有四个属性,分别介绍一下作用:

  • prefix:指定sql的前缀,该前缀会被放在trim标签拼接好的sql前面

  • suffix:指定sql的后缀,该后缀会被放在trim标签拼接好的sql后面

  • prefixOverrides:前缀覆盖,MyBatis会自动去除属性前的指定字符串

  • suffixOverrides:后缀覆盖,MyBatis会自动去除属性后的指定字符串

在这里我们将前缀指定为where,并指定后缀覆盖为and,就能解决之前的问题了,执行结果:


   
  1. ==> Preparing: select * from tbl_employee where last_name like ?
  2. ==> Parameters: %c%( String)
  3. <== Columns: id, last_name, gender, email, d_id
  4. <== Row:  1, jack, 1, jack@qq.com, 1
  5. <== Total:  1
  6. Employee(id= 1, lastName= null, gender= 1, email=jack@qq.com)

05. choose

该标签用于分支操作,与if不同的是,choose只有一个会被执行,若有一个choose满足条件,则其它choose都将失效,比如:根据传递过来的参数进行员工信息查询,如果携带id,则根据id查询,如果携带lastName,则根据lastName查询,实现如下:

List<Employee> getEmpsByConditionChoose(Employee employee);

   
  1. <select id= "getEmpsByConditionChoose" resultType= "com.wwj.mybatis.bean.Employee">
  2.   select *
  3.    from tbl_employee
  4.   <where>
  5.     <choose>
  6.       <when test= "id != null">
  7.         id = ${id}
  8.       </when>
  9.       <when test= "lastName != null">
  10.         last_name like #{lastName}
  11.       </when>
  12.       <otherwise>
  13.       </otherwise>
  14.     </choose>
  15.   </where>
  16. </select>

choose标签类似于带break的switch语句,otherwise标签表示其它条件都不满足则执行该标签内容,编写业务代码:


   
  1. Employee employee = new Employee( 1, "%c%", null, null);
  2. List<Employee> emps = mapper.getEmpsByConditionChoose(employee);
  3. for (Employee emp : emps) {
  4.     System.out.println(emp);
  5. }

这里同时携带了id和lastName,sql语句会如何拼接呢?因为id的判断在lastName之前,所以id分支已经满足条件,其它分支都会失效,故sql语句如下:


   
  1. ==> Preparing: select * from tbl_employee WHERE id = 1 
  2. ==> Parameters: 
  3. <== Columns: id, last_name, gender, email, d_id
  4. <== Row:  1, jack, 1, jack@qq.com, 1
  5. <== Total:  1

若是什么参数都不带,则执行otherwise,sql什么都不拼接,故查询所有数据。

06. set

学会了如何动态查询之后,我们来尝试一下动态更新,即:根据传递过来的参数进行员工信息更新,携带了什么参数就更新什么参数,实现如下:

void updateEmp(Employee employee);

   
  1. <update id= "updateEmp">
  2.   update tbl_employee
  3.   set
  4.   < if test= "lastName != null">
  5.     last_name = #{lastName},
  6.   </ if>
  7.   < if test= "email != null">
  8.     email = #{email},
  9.   </ if>
  10.   < if test= "gender != null">
  11.     gender = #{gender}
  12.   </ if>
  13.   <where>
  14.     id = #{id}
  15.   </where>
  16. </update>

这样实现的话,当参数均携带的时候不会出现问题,然而这种情况:


   
  1. Employee employee = new Employee( 1, "tom", null, null);
  2. mapper.updateEmp(employee);

因为lastName后面的参数都为null,导致   被直接拼在了id的前面,造成语法错误,来看看生成的sql:


   
  1. ==> Preparing: update tbl_employee set last_name = ?, WHERE id = ?
  2. ==> Parameters: tom( String), 1( Integer)

为此,我们可以使用set标签,将这些动态更新的内容放在set标签内就能够解决这一问题:


   
  1. <update id= "updateEmp">
  2.   update tbl_employee
  3.   <set>
  4.     < if test= "lastName != null">
  5.       last_name = #{lastName},
  6.     </ if>
  7.     < if test= "email != null">
  8.       email = #{email},
  9.     </ if>
  10.     < if test= "gender != null">
  11.       gender = #{gender}
  12.     </ if>
  13.   </set>
  14.   <where>
  15.     id = #{id}
  16.   </where>
  17. </update>

这里也可以使用trim标签解决逗号拼接的问题,方式如下:


   
  1. <update id= "updateEmp">
  2.   update tbl_employee
  3.   <trim prefix= "set" suffixOverrides= ",">
  4.     < if test= "lastName != null">
  5.       last_name = #{lastName},
  6.     </ if>
  7.     < if test= "email != null">
  8.       email = #{email},
  9.     </ if>
  10.     < if test= "gender != null">
  11.       gender = #{gender}
  12.     </ if>
  13.   </trim>
  14.   <where>
  15.     id = #{id}
  16.   </where>
  17. </update>

07. foreach

有时候我们需要同时查询多个值的数据,为此,可以使用foreach实现,比如 查询id为1、2、3的员工信息,实现如下:


   
  1. List<Employee> getEmpsByConditionForeach(@Param( "ids") List< Integer> ids);
  2. <select id= "getEmpsByConditionForeach" resultType= "com.wwj.mybatis.bean.Employee">
  3.   select *
  4.    from tbl_employee
  5.   where id in(
  6.   < foreach collection= "ids" item= "id" separator= ",">
  7.      #{id}
  8.   </ foreach>
  9.   )
  10. </select>

首先collection属性指定需要遍历的集合,item属性指定遍历出的每个元素的名字,separator属性指定的是每个遍历出的元素的分隔符,一定不要忘记in后面的小括号,foreach也支持将小括号写在标签内:


   
  1. <select id= "getEmpsByConditionForeach" resultType= "com.wwj.mybatis.bean.Employee">
  2.   select *
  3.    from tbl_employee
  4.   where id in
  5.   < foreach collection= "ids" item= "id" separator= "," open= "(" close= ")">
  6.      #{id}
  7.   </ foreach>
  8. </select>

编写业务代码:


   
  1. List<Employee> emps = mapper.getEmpsByConditionForeach(Arrays.asList( 1, 2, 3));
  2. for (Employee emp : emps) {
  3.     System.out.println(emp);
  4. }

执行结果:


   
  1. ==> Preparing: select * from tbl_employee where id in ( ? , ? , ? )
  2. ==> Parameters:  1( Integer), 2( Integer), 3( Integer)
  3. <== Columns: id, last_name, gender, email, d_id
  4. <== Row:  1, tom, 1, jack@qq.com, 1
  5. <== Row:  3, tom, 1, tom@qq.com, 2
  6. <== Total:  2
  7. Employee(id= 1, lastName= null, gender= 1, email=jack@qq.com)
  8. Employee(id= 3, lastName= null, gender= 1, email=tom@qq.com)

借助foreach标签我们还能够实现批量插入,实现如下:

void addEmpBatch(@Param("emps") List<Employee> emps);

   
  1. <insert id= "addEmpBatch">
  2.   insert into tbl_employee(last_name,gender,email)
  3.   values
  4.   < foreach collection= "emps" item= "emp" separator= ",">
  5.     ( #{emp.lastName},#{emp.gender},#{emp.email})
  6.   </ foreach>
  7. </insert>

编写业务代码:


   
  1. List<Employee> emps = new ArrayList<>();
  2. emps.add( new Employee( null, "jack", '1', "jack@qq.com"));
  3. emps.add( new Employee( null, "jerry", '1', "jerry@qq.com"));
  4. mapper.addEmpBatch(emps);

执行结果:


   
  1. ==> Preparing: insert into tbl_employee(last_name,gender,email) values (?,?,?) , (?,?,?)
  2. ==> Parameters: jack( String), 1( String), jack@qq.com( String), jerry( String), 1( String), jerry@qq.com( String)
  3. <== Updates: 2

也可以通过执行多条sql来实现批量插入:


   
  1. <insert id= "addEmpBatch">
  2.   < foreach collection= "emps" item= "emp" separator= ";">
  3.     insert into tbl_employee(last_name, gender, email)
  4.     values ( #{emp.lastName}, #{emp.gender}, #{emp.email})
  5.   </ foreach>
  6. </insert>

sql语句之间用 ; 分隔,但是MySQL默认是不支持这种语法的,为此,需要设置一个参数使其支持该语法:

jdbc.url=jdbc:mysql:///mybatis?allowMultiQueries=true

MyBatis提供了两个内置参数用于辅助操作,也就是说,即使方法没有传递任何参数,我们也能够在操作标签中取出这两个参数进行处理:

  • _parameter:所有参数,单个参数情况下,_parameter就是该参数;多个参数情况下,_parameter就是封装所有参数的Map集合

  • _databaseId:若配置了databaseIdProvider 标签,则该参数就是当前数据库的别名

08. bind

MyBatis提供了bind标签用于将表达式的值绑定到一个变量中,用法如下:


   
  1. <select id= "getEmpsByConditionChoose" resultType= "com.wwj.mybatis.bean.Employee">
  2.   <bind name= "_lastName" value= "'%'+ lastName + '%'"/>
  3.   select *
  4.    from tbl_employee
  5.   <where>
  6.     <choose>
  7.       <when test= "id != null">
  8.         id = ${id}
  9.       </when>
  10.       <when test= "lastName != null">
  11.         last_name like #{_lastName}
  12.       </when>
  13.       <otherwise>
  14.       </otherwise>
  15.     </choose>
  16.   </where>
  17. </select>

这里使用bind标签定义了一个变量_lastName,其值为lastName值前后拼接 % ,接下来就可以引用该变量,此时调用方法进行模糊查询就不需要自己拼接 %c% 了,而是直接传入字符串 c 即可。

09. sql

该标签能够抽取可重用的sql片段,使Mapper文件更加简洁:


   
  1. <sql id= "insertColumn">
  2.   last_name,gender,email
  3. </sql>

接下来若是想使用这一字符串,直接用include标签包含进来即可:


   
  1. <insert id= "addEmpBatch">
  2.   insert into tbl_employee(
  3.   < include refid= "insertColumn"></ include>
  4.   )
  5.   values
  6.   < foreach collection= "emps" item= "emp" separator= ",">
  7.     ( #{emp.lastName},#{emp.gender},#{emp.email})
  8.   </ foreach>
  9. </insert>
公众号运营至今,离不开小伙伴们的支持。为了给小伙伴们提供一个互相交流的平台,特地开通了官方交流群。关注公众号「Java后端」回复「进群」即可。

   
  1. 推荐阅读 
  2. 1. 推荐几个好玩的 GitHub 项目
  3. 2. 推荐几个程序员常用的软件
  4. 3. 图解 Spring 解决循环依赖

转载:https://blog.csdn.net/qq_37217713/article/details/114314830
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场