Javaperson

MyBatis笔记 | 详解动态SQL

2019-12-11  本文已影响0人  一颗白菜_

我们想,对于SQL映射文件中的sql语句,能够根据传入的值的不同来动态的拼接sql语句。此时就可以使用到动态SQL。

环境的准备

新建EmployeeMapperDynamicSQL接口:

package com.cerr.mybatis.dao;

import com.cerr.mybatis.Employee;

import java.util.List;

public interface EmployeeMapperDynamicSQL {

}

新建EmployeeMapperDynamicSQL.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="com.cerr.mybatis.dao.EmployeeMapperDynamicSQL">
    
</mapper>


使用if标签实现表达式判断

我们可以使用<if>标签来对表达式进行判定,然后作出对应的SQL拼接。<if>标签有一个test属性用来判断表达式用的,这里的表达式采用的是OGNL表达式,对于OGNL表达式的介绍可以参考该文章的部分内容:Struts2学习笔记 | 值栈和OGNL

扩展

我们应该注意转义字符,例如""应该写为&quot;&quot;

我们在接口中新增一个方法:

    //携带了哪个字段,查询条件就带上这个字段
    public List< Employee > getEmpsByConditionIf(Employee employee);

测试方法:

package com.cerr.mybatis;
import com.cerr.mybatis.dao.*;
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.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MyBatisTest {

    //获取SQLSessionFactory
    public SqlSessionFactory getSqlSessionFactory() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        return new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testDynamicSql() throws IOException {
        SqlSessionFactory factory = getSqlSessionFactory();
        SqlSession session = factory.openSession();
        try{
            EmployeeMapperDynamicSQL mapper = session.getMapper(EmployeeMapperDynamicSQL.class);
            Employee employee = new Employee(1,"%e%",null,null);
            List<Employee> employees = mapper.getEmpsByConditionIf(employee);
            for(Employee e : employees){
                System.out.println(e);
            }
        }finally {
            session.close();
        }
    }
}

在SQL映射文件中的配置:

    <!-- 查询员工,要求:携带了哪个字段,查询条件就带上这个字段 -->
    <select id="getEmpsByConditionIf" resultType="com.cerr.mybatis.Employee">
        select * from tb1_employee
        where
            <if test="id!=null">
                id=#{id}
            </if>
            <if test="lastName!=null and lastName!=''">
                and last_name like #{lastName}
            </if>
            <if test="email!=null and email.trim()!=&quot;&quot;">
                and email=#{email}
            </if>
            <!-- ognl会进行字符串与数字的转换判断 -->
            <if test="gender==0 or gender==1">
                and gender=#{gender}
            </if>
    </select>

但是这个配置文件的话,有个问题,每次传入的id必须不为空,如果id为空的话,假设后面任意一个不为空(例如lastName),则sql语句为select * from tb1_employee where and last_name like ?,很明显多了个and,该sql语句是错误的,我们可以使用下面的where标签来改善这个问题。

使用<where>标签,MyBatis就会将<where>标签里面拼接的sql中多余的andor去掉。
正确用法:

    <select id="getEmpsByConditionIf" resultType="com.cerr.mybatis.Employee">
        select * from tb1_employee
        <where>
            <if test="id!=null">
                id=#{id}
            </if>
            <if test="lastName!=null and lastName!=''">
                and last_name like #{lastName}
            </if>
            <if test="email!=null and email.trim()!=&quot;&quot;">
                and email=#{email}
            </if>
            <!-- ognl会进行字符串与数字的转换判断 -->
            <if test="gender==0 or gender==1">
                and gender=#{gender}
            </if>
        </where>
    </select>

但是其只会去掉第一个多余的andor,如果在<if>标签中每次都把andor写在后面,则该标签也无法正常去除多余的andor,所以应该在<if>标签中每次都把andor写在前面。例如下面这种情形就无法正常去除:

<!-- 查询员工,要求:携带了哪个字段,查询条件就带上这个字段 -->
    <select id="getEmpsByConditionIf" resultType="com.cerr.mybatis.Employee">
        select * from tb1_employee
        <where>
            <if test="id!=null">
                id=#{id} and
            </if>
            <if test="lastName!=null and lastName!=''">
                last_name like #{lastName} and
            </if>
            <if test="email!=null and email.trim()!=&quot;&quot;">
                email=#{email} and
            </if>
            <!-- ognl会进行字符串与数字的转换判断 -->
            <if test="gender==0 or gender==1">
                gender=#{gender}
            </if>
        </where>
    </select>

使用trim标签实现字符串截取

<trim>标签体中是整个字符串拼串后的结果,有四个属性:

配置可修改如下:

    <select id="getEmpsByConditionTrim" resultType="com.cerr.mybatis.Employee">
        select * from tb1_employee
        <trim prefix="where" suffixOverrides="and">
            <if test="id!=null">
                id=#{id} and
            </if>
            <if test="lastName!=null and lastName!=''">
                 last_name like #{lastName} and
            </if>
            <if test="email!=null and email.trim()!=&quot;&quot;">
                 email=#{email} and
            </if>
            <!-- ognl会进行字符串与数字的转换判断 -->
            <if test="gender==0 or gender==1">
                 gender=#{gender}
            </if>
        </trim>
    </select>

测试方法:

package com.cerr.mybatis;
import com.cerr.mybatis.dao.*;
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.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MyBatisTest {

    //获取SQLSessionFactory
    public SqlSessionFactory getSqlSessionFactory() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        return new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testDynamicSql() throws IOException {
        SqlSessionFactory factory = getSqlSessionFactory();
        SqlSession session = factory.openSession();
        try{
            EmployeeMapperDynamicSQL mapper = session.getMapper(EmployeeMapperDynamicSQL.class);
            Employee employee = new Employee(null,"%e%",null,null);
            List<Employee> employees = mapper.getEmpsByConditionTrim(employee);
            for(Employee e : employees){
                System.out.println(e);
            }
        }finally {
            session.close();
        }
    }
}

使用choose标签来实现分支选择

相当于java中的switch语句,但是是有加break的,可配合<when><otherwise>标签使用。

我们现在想实现以下功能,如果传入id就用id查,如果传入lastName就用lastName查,只会选一个。
接口新增方法:

public List<Employee> getEmpsByConditionChoose(Employee employee);

SQL映射文件:

    <select id="getEmpsByConditionChoose" resultType="com.cerr.mybatis.Employee">
        select * from tb1_employee
        <where>
            <!-- 如果传入`id`就用`id`查,如果传入`lastName`就用`lastName`查,只会选一个 ,如果这两个都没有传,则查女生-->
            <choose>
                <when test="id!=null">
                    id = #{id}
                </when>
                <when test="lastName!=null">
                    last_name like #{lastName}
                </when>
                <otherwise>
                    gender = 0
                </otherwise>
            </choose>
        </where>
    </select>

测试方法:

    @Test
    public void testDynamicSql() throws IOException {
        SqlSessionFactory factory = getSqlSessionFactory();
        SqlSession session = factory.openSession();
        try{
            EmployeeMapperDynamicSQL mapper = session.getMapper(EmployeeMapperDynamicSQL.class);
            Employee employee = new Employee(null,"%e%",null,null);
            List<Employee> employees = mapper.getEmpsByConditionChoose(employee);
            for(Employee e : employees){
                System.out.println(e);
            }
        }finally {
            session.close();
        }
    }

测试方法中传入的是lastName,则发送的sql为:

Preparing: select * from tb1_employee WHERE last_name like ? 

结果:


如果改为Employee employee = new Employee(null,null,null,null);,则发送的sql为:

select * from tb1_employee WHERE gender = 0

结果如下:



使用if与set标签来实现动态的update语句

<set>标签可以替换掉我们之前update语句中的set,并且可以支持将后面多余的,去掉。

我们有一个需求,对于tb1_employee表,我们要更新其一条记录,但是我们的目标是,传入哪些参数,我们就更新哪些字段。我们如果单纯用<if>标签的话,就是下面的配置:

    <update id="updateEmp" >
        update tb1_employee set
        <if test="lastName!=null">
            last_name = #{lastName},
        </if>
        <if test="email!=null">
            email=#{email},
        </if>
        <if test="gender!=null">
            gender=#{gender}
        </if>
            where id=#{id}
    </update>

但是这样会出问题,假设我只传了一个last_name字段,则SQL语句是这样的:

update tb1_employee set last_name = ? , where id = ?

可以看到在last_name = ?后面多了一个,,这个时候我们就可以使用<set>标签来进行改进了,改进版如下:

    <update id="updateEmp" >
        update tb1_employee
        <set>
            <if test="lastName!=null">
                last_name = #{lastName},
            </if>
            <if test="email!=null">
                email=#{email},
            </if>
            <if test="gender!=null">
                gender=#{gender}
            </if>
        </set>
        where id=#{id}
    </update>

此时如果只传入last_name,也不会有多余的逗号,发送的SQL为:

  update tb1_employee set last_name = ? where id=?

同样也可以使用<trim>标签来进行修改,加上前缀的set,并且取出后缀中多余的,

    <update id="updateEmp" >
        update tb1_employee
        <trim prefix="set" suffixOverrides=",">
            <if test="lastName!=null">
                last_name = #{lastName},
            </if>
            <if test="email!=null">
                email=#{email},
            </if>
            <if test="gender!=null">
                gender=#{gender}
            </if>
        </trim>
        where id=#{id}
    </update>

剩下的代码:
接口方法:

public void updateEmp(Employee employee);

测试方法:

package com.cerr.mybatis;
import com.cerr.mybatis.dao.*;
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.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MyBatisTest {

    //获取SQLSessionFactory
    public SqlSessionFactory getSqlSessionFactory() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        return new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testDynamicSql() throws IOException {
        SqlSessionFactory factory = getSqlSessionFactory();
        SqlSession session = factory.openSession();
        try{
            EmployeeMapperDynamicSQL mapper = session.getMapper(EmployeeMapperDynamicSQL.class);
            Employee employee = new Employee(1,"Admin",null,null);
            mapper.updateEmp(employee);
            session.commit();
        }finally {
            session.close();
        }
    }
}

foreach标签

<foreach>标签可以使用遍历集合,有如下几个属性:

使用#{变量名}就能取出变量的值也就是当前遍历出的元素。

使用foreach标签来遍历集合

我们下面想实现一个循环查询,即我在方法中传入一个id数组,然后使用<foreach>来让我们的查询条件可以动态的改变,即我传入几个id就查那几个id对应的记录,例如如下的sql语句:

select * from tb1_employee where id in(1,4);
select * from tb1_employee where id in(1,2,3,4,5,6);

根据传入的id的集合来查询对应的记录。

首先接口的方法如下:

public List<Employee> getEmpsByConditionForeach(@Param("ids") List<Integer> ids);

在该接口中,我们使用了@Param注解来传入命名参数,以便我们SQL映射文件的collection属性来使用该集合。有关参数处理可以看这篇文章点击查看:MyBatis笔记 | 详解参数处理(多种类型的参数处理、源码分析、读取参数的两种格式的区别)

SQL映射文件如下:

    <select id="getEmpsByConditionForeach" resultType="com.cerr.mybatis.Employee">
        select * from tb1_employee where id in
            <foreach collection="ids" item="item_id" separator="," open="(" close=")">
                #{item_id}
            </foreach>
    </select>

测试方法:

package com.cerr.mybatis;
import com.cerr.mybatis.dao.*;
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.Test;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class MyBatisTest {

    //获取SQLSessionFactory
    public SqlSessionFactory getSqlSessionFactory() throws IOException {
        String resource = "mybatis-config.xml";
        InputStream inputStream = Resources.getResourceAsStream(resource);
        return new SqlSessionFactoryBuilder().build(inputStream);
    }

    @Test
    public void testDynamicSql1() throws IOException {
        SqlSessionFactory factory = getSqlSessionFactory();
        SqlSession session = factory.openSession();
        try{
            EmployeeMapperDynamicSQL mapper = session.getMapper(EmployeeMapperDynamicSQL.class);
            List<Employee> employees = mapper.getEmpsByConditionForeach(Arrays.asList(1,4));
            for(Employee e : employees){
                System.out.println(e);
            }
        }finally {
            session.close();
        }
    }

因为我们此时方法传递的List集合只有两个元素,因此发送的sql如下:

Preparing: select * from tb1_employee where id in( ? , ? ) 

结果:


mysql下使用foreach实现批量插入的两种方式

第一种是直接使用insert into table_name(...) values(),(),()这种语法格式,即是使用foreach遍历values后面的括号。
接口方法如下:

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

配置如下:

<insert id="addEmps">
        insert into tb1_employee(last_name,email,gender,d_id)
        values
        <foreach collection="emps" item="emp" separator=",">
            (#{emp.lastName},#{emp.email},#{emp.gender},#{emp.department.id})
        </foreach>

</insert>

第二种是每插入一条语句就发一条insert语句,即使用foreach循环遍历insert语句,中间用;分割,接口同上。
使用这种方法的话需要在数据库连接中设置一个allowMultiQueries属性,表示允许多个查询之间使用;分割,即url为:

jdbc:mysql://localhost:3306/mybatis?allowMultiQueries=true

配置如下:

<insert id="addEmps">
        <foreach collection="emps" item="emp" separator=";">
            insert into tb1_employee(last_name,email,gender,d_id)
            values
            (#{emp.lastName},#{emp.email},#{emp.gender},#{emp.department.id})
        </foreach>
</insert>

两个内置参数

不止是方法传递过来的参数可以被用来判断,取值。MyBatis默认还有两个内置参数:

例如我们现在想编写一个sql映射:当数据库为mysql、Oracle时使用不同的sql,且当传入的Employee参数为空时,就不带条件查询,那么就可以使用到上面这两个参数了。
接口如下:

public List<Employee> getEmpsTestInnerParameter(Employee employee);

配置如下:

<select id="getEmpsTestInnerParameter" resultType="com.cerr.mybatis.Employee">
        <if test="_databaseId=='mysql'">
            select * from tb1_employee
            <if test="_parameter!=null">
                where last_name = #{_parameter.lastName}
            </if>
        </if>

        <if test="_databaseId=='oracle'">
            select * from employee
        </if>
</select>

当我们传入的Employee不为空时,sql语句为:

select * from tb1_employee where last_name = ? 

当传入的Employee为空时,sql语句为:

select * from tb1_employee

使用bind标签来进行动态绑定

可以将OGNL表达式的值绑定到一个变量中,方便后面引用这个变量的值。

<select id="getEmpsTestInnerParameter" resultType="com.cerr.mybatis.Employee">
        <bind name="_lastName" value="'%'+lastName+'%'"/>
        select * from tb1_employee
        <if test="_parameter!=null">
                where last_name like #{_lastName}
        </if>
</select>

上述SQL映射中的<bind>标签将传入的lastName值的前后加了%并赋值给_lastName,在sql语句中我们就可以使用#{_lastName}参数来做模糊查询的条件。


使用sql标签来抽取重用的sql片段

在sql语句中,总是存在一些可以重用的sql片段。我们可以使用<sql>标签来抽取这些可重用的sql片段,然后使用<include>标签来引用它,<include>里面还可以自定义一些property,在<sql>标签内部就可以通过${变量名}来使用这些自定义属性(不能使用#{}来取这些自定义属性的值)

<sql id="insertColumn">
        last_name,email,gender,d_id
</sql>

上述代码,我们使用sql抽取了tb1_employee表的部分字段,接下来我们要插入表的时候就可以直接引用:

    <insert id="addEmps">
        <foreach collection="emps" item="emp" separator=";">
            insert into tb1_employee(
                <include refid="insertColumn"/>
            )
            values
            (#{emp.lastName},#{emp.email},#{emp.gender},#{emp.department.id})
        </foreach>
    </insert>

相当于:

    <insert id="addEmps">
        <foreach collection="emps" item="emp" separator=";">
            insert into tb1_employee(
                  last_name,email,gender,d_id
            )
            values
            (#{emp.lastName},#{emp.email},#{emp.gender},#{emp.department.id})
        </foreach>
    </insert>

许多可以在sql里面写的标签在该标签里面也可以用。

上一篇下一篇

猜你喜欢

热点阅读