12月30, 2018

MyBatis手把手跟我做(三) 动态SQL

MyBatis(三) 动态SQL标签

上一章已经实现了最简单的CRUD的操作,不过还是有一些细节问题需要进一步的学习

一.#{}${}

在前面的章节中,大家已经看过很多次#()这个符号了,虽然没有专门的去讲解,但是也应该能猜出#{}应该就是一个参数的占位符,是的,大家猜测的没错,不过这个占位符除了#{}之外,还有一个${},两者都是起到占位的作用,但又是完全不同的. 简单来说, #{}其实就相当于我们之前在JDBC代码中使用了PreparedStatement,并在SQL语句中使用了 ? 替代符

${}的占位,就相当于SQL语句直接使用了字符串拼接,而且还需要自己加上``哦.

上面的意思我们用伪代码形容一下

select * from t_user where username=#{username}
就相当于
select * from t_user where username=?
而且会在随后的代码执行中自动用你传入的 `username` 参数将`?`替代掉,比如`username`的值是`张三`
select * from t_user where username='张三'

select * from t_user where username=${username}
比如 参数 username 的值是张三,那就相当于
select * from t_user where username=张三
注意这里是错误的哦,因为张三是个字符串,你需要自己加上单引号
select * from t_user where username='${username}'才是正确的写法

所以,总结来说: #{}:解析为一个JDBC预编译语句(PreparedStatement)的参数标记,一个#()会被解析为一个参数占位符?,在数据库中发生参数的替换

${}: 仅仅为一个纯粹的String字符换的替换,而且在动态SQL的解析阶段就讲变量进行替换,而且如果参数就是一个简单类型的话(意思是不是一个自定义对象,就是一个int,或者String),那么参数的占位符只能使用value,也就是上面的 username="张三",如果就只有这么一个值,使用 ${} 替代符就只能使用value占位,也就是要写为 select * from t_user where username=${value}

因此一般情况下,我们当然优先使用#(),还能一定程度上防止SQL注入,但是有些时候使用${}却很方便,比如在模糊查询的时候

...
<!-- 根据名称模糊查询用户信息 -->
<select id="getUserByName" parameterType="string" resultMap="userMap">
    select * from t_user where username like '%${value}%'
</select>
...

下面的截图,展示了上面文字描述的一些问题 2018-12-27_16-51-55 2018-12-27_17-01-33 2018-12-27_17-06-43

2018-12-27_17-21-20

2018-12-27_17-12-33

二.动态SQL

上面的例子只是根据用户的名字进行了模糊查询,这个时候问题就来了,如果要使用多条件查询呢?那就需要用到MyBatis提供的动态SQL功能了,通过 if, choose, when, otherwise, trim, where, set, foreach标签,可组合成非常灵活的SQL语句

1. whereif

还是直接通过应用场景来解释,相信大家之前都写过多条件查询的SQL语句了,基本的思考点就是到底有哪些条件?哪些条件不为空的时候才进行SQL拼接?多条件后面的and或者or该怎么搞定?这个问题,在大家的学习过程中,最早期的代码应该是这么写的

......
String sql = "select * from t_user where 1=1 "
if(username != null && !"".equals(username)){
    sql += " and username like '%" + username + "%'";
}

if(userTel != null && !"".equals(userTel)){
    sql += " and userTel ='" + userTel + "'";
}
...

在早期的JDBC代码中,为了避免SQL语句后面拼接and的问题,一般都会人为的在前面加上一个条件1=1,使用MyBatis其实也跳不过这个问题,不过我们使用MyBatis自带的标签来解决这个问题

<!--多条件查询动态SQL-->
<select id="getUserBySelective" parameterType="user" resultMap="userMap">
    select * from t_user
    <where>
        <if test="id >= 1">
            id=#{id}
        </if>
        <if test="userTel != null">
            and user_tel like '%${userTel}%'
        </if>
        <if test="username != null">
            and username like '%${username}%'
        </if>
        <if test="registrationTime != null">
            and Date_Format(registration_time,'%Y-%m-%d')=#{registrationTime}
        </if>
    </where>
</select>

上面的语句就用到了MyBatis的where和if标签,具体标签的含义其实不用多做解释大家也能看懂,而且最好的地方是完美的解决了之前and拼接的问题,下面的截图给大家展示了这些效果 2018-12-28_14-42-40

2018-12-28_15-10-21

2018-12-28_15-18-41

2. trimif

where标签也完全可以用trim标签替代,一般情况下的写法是这个样子的

<trim prefix="WHERE" prefixOverrides="AND |OR ">
  ... 
</trim>

其实关键就是prefixprefixOverrides这两个属性 prefix: 前缀,这里的意思就是SQL语句加上where prefixOverrides: 去掉第一个and或者是or,就是后面的拼接语句中出现了and或者or,如果出现在第一个条件中就去掉

上面的代码,我们通过trim标签替换

<trim prefix="WHERE" prefixOverrides="AND |OR ">
    <if test="id >= 1">
        id=#{id}
    </if>
    <if test="userTel != null">
        and user_tel like '%${userTel}%'
    </if>
    <if test="username != null">
        and username like '%${username}%'
    </if>
    <if test="registrationTime != null">
        and Date_Format(registration_time,'%Y-%m-%d')=#{registrationTime}
    </if>
</trim>

2018-12-28_15-40-15

3. setif

既然查询是多条件的,我们回过头看看之前写过的新增insert和修改update方法,按照之前的写法,如果有条件不传,会出现什么问题,我们看一下之前的修改方法 2018-12-28_16-14-46

这样肯定和实际情况不符,我们新加一个修改方法,根据传入的值进行判断修改,这样改的关键点其实还是判断要修改的值是否为空,不为空再进行修改,问题的关键点还是多个条件判断的时候,最后一个逗号(,)截取的问题,在MyBatis中,使用set标签 2018-12-28_16-38-37 2018-12-28_16-44-36

同样,<trim>标签也可以完全替换<set>

<update id="updateUserByIdSelective" parameterType="user">
  update t_user
  <trim prefix="set" suffixOverrides=",">
      <if test="userTel != null">
          user_tel=#{userTel},
      </if>
      <if test="username != null">
          username=#{username},
      </if>
      <if test="password != null">
          password=md5(#{password}),
      </if>
      <if test="registrationTime != null">
          registration_time=#{registrationTime}
      </if>
  </trim>
  where id=#{id}
</update>

suffixOverrides: 去掉最后一个逗号(,)后缀

在新增语句中,也是一样,如果只是选择性的要插入一些字段(当然前提是数据库中该字段可以为null),看一下之前新增数据的效果 2018-12-29_11-00-36

如果要选择性的拼接SQL,新增的SQL语句拼接会涉及到表字段,字段对应的值以及左括号,逗号,右括号,这样拼接判断的时候复杂度稍微高一些,所以,这里使用<trim>标签是最好的选择

<!--根据传入的参数新增用户信息-->
<insert id="insertUserSelective" parameterType="user" useGeneratedKeys="true" keyProperty="id">
    insert into t_user
    <trim prefix="(" suffix=")" suffixOverrides="," >
        <if test="id != null" >
            id,
        </if>
        <if test="userTel != null">
            user_tel,
        </if>
        <if test="username != null" >
            username,
        </if>
        <if test="password != null" >
            password,
        </if>
        <if test="registrationTime != null" >
            registration_time,
        </if>
    </trim>
    <trim prefix="values (" suffix=")" suffixOverrides="," >
        <if test="id != null" >
            #{id,jdbcType=INTEGER},
        </if>
        <if test="userTel != null" >
            #{userTel,jdbcType=VARCHAR},
        </if>
        <if test="username != null" >
            #{username,jdbcType=VARCHAR},
        </if>
        <if test="password != null" >
            md5(#{password,jdbcType=VARCHAR}),
        </if>
        <if test="registrationTime != null" >
            #{registrationTime,jdbcType=VARCHAR},
        </if>
    </trim>
</insert>

2018-12-29_11-14-51

4. SQLinclude

有时候可能某个 sql 语句我们用的特别多,为了增加代码的重用性,简化代码,我们需要将这些代码抽取出来,然后使用时直接调用。 比如上面新增语句的字段条件判断,插入语句实在写的太多,可以将这部分的代码提出来,直接写成代码片段,到时候再用<include>标签引用就可以了 2018-12-29_17-07-54

这种用法特别是在写查询语句的时候,字段名较多的情况下,把经常要现实出来的字段用SQL标签引入,在写SQL语句的时候,再直接通过include引入字段就可以了

5. choose(when,otherwise)

上面的whereset标签都是在和if标签配合,判断标签除了if之外,还有choose(when,otherwise)标签,choose(when,otherwise)其实就类似于java的switch语句,把之前的查询语句,从where...if...的组合换成 where...choose(when,otherwise),但是要注意两者之间的区别,choose(when,otherwise)同时只能成立一个条件

 <!--多条件查询动态SQL,使用where...choose(when...otherwise)组合-->
<select id="getUserBySelective2" parameterType="user" resultMap="userMap">
    select * from t_user
    <where>
        <choose>
            <when test="id >= 1">
              id=#{id}
            </when>
            <when test="userTel != null">
              and user_tel like '%${userTel}%'
            </when>
            <when test="username != null">
              and username like '%${username}%'
            </when>
            <otherwise>
              and Date_Format(registration_time,'%Y-%m-%d')=#{registrationTime}
            </otherwise>
        </choose>
    </where>
</select>

2018-12-29_13-36-56

6. foreach

先来捋一捋代码场景,比如同时要查询多个值的场景,意思是SQL语句要写成类似于下面的这个样子:

select * from t_user where id=1 or id=4 or id=5 or id=8

或者

select * from t_user where id in(1, 4, 5, 8)

注意: 这个样子的SQL语句,为了传输值方便,不破坏User类,我们再封装一个Bean类,就叫做查询Bean,这个Bean就是为了查询而服务的,什么意思呢?界面上可能会有各种条件的查询,比如注册的开始日期,结束日期,根据这种情况进行查询,但是我们现在的User类其实只是完全和数据库对应的类,User类里只有一个注册日期,没有什么注册开始日期和结束日期的概念,但是在查询的时候却需要查询这些东西,所以干脆就专门建一个实体类,来处理封装界面上要查询的内容,这样就可以方便的将界面上的数据传输给Dao层.这其实是分层架构中VO,PO,DO,DTO等等领域模型的概念,有兴趣的可以自己查询了解一下

无论如果,为了封装方便,这里新创建一个类 QueryBean.java:

import java.util.List;

public class QueryBean {
    //封装要查询的多个用户的id
    private List<Integer> ids;

    public List<Integer> getIds() {
        return ids;
    }

    public void setIds(List<Integer> ids) {
        this.ids = ids;
    }
}

在UserMapper.xml中添加根据多个id查询的方法

<select id="getUserByQueryIds" parameterType="queryBean" resultMap="userMap">
    select * from t_user
    <where>
        <!--
            collection:指定输入对象中的集合属性
            item:每次遍历生成的对象
            open:开始遍历时的拼接字符串
            close:结束时拼接的字符串
            separator:遍历对象之间需要拼接的字符串
            select * from t_user where (id=1 or id=4 or id=5 or id=8)
          -->
        <foreach collection="ids" item="id" open="(" close=")" separator="or">
            id=#{id}
        </foreach>
    </where>
</select>

注意下图中,foreach标签中每个属性的作用

2018-12-30_11-46-07

也可以稍微换一种写法

<select id="getUserByQueryIds" parameterType="queryBean" resultMap="userMap">
    select * from t_user
    <where>
        <!--
            collection:指定输入对象中的集合属性
            item:每次遍历生成的对象
            open:开始遍历时的拼接字符串
            close:结束时拼接的字符串
            separator:遍历对象之间需要拼接的字符串
            select * from t_user where (id=1 or id=4 or id=5 or id=8)

        <foreach collection="ids" item="id" open="(" close=")" separator="or">
            id=#{id}
        </foreach>
        -->

        <!-- select * from t_user where id in (1, 4, 5, 8) -->
        <foreach collection="ids" item="id" open="id in (" close=")" separator=",">
            #{id}
        </foreach>
    </where>
</select>

2018-12-30_15-05-54

本文链接:http://www.yanhongzhi.com/post/mybatis-dynamicsql.html

-- EOF --

Comments