if用法

if 标签通常用于 WHERE 语句中,通过判断参数值来决定是否使用某个查询条件,它也经常用于 UPDATE 语句中判断是否更新某一个字段,还可以在 INSERT 语句中用来判断是否插入某个字段的值。

在WHERE条件中使用if

假设现在有一个新的需求:实现一个用户管理高级查询功能,根据输入的条件去检索用户信息。这个功能还需要支持以下三种情况:当只输入用户名时,需要根据用户名进行模糊查询;当只输入邮箱时,根据邮箱进行完全匹配;当同时输入用户名和邮箱时,用这两个条件去查询匹配的用户。

如果仍然按照前面章节中介绍的方法去编写代码,可能会写出如下方法。

<select id="selectByUser" resultType="tk.mybatis.simple.model.SysUser">
    select id,
        user_name userName,
        user_password userPassword,
        user_email userEmail,
        user_info userInfo,
        head_img headImg,
        create_time createTime
    from sys_user
    where user_name like concat('%', #{userName}, '%')
        and user_email = #{userEmail}
</select>

当同时输入 userName 和 userEmail 这两个条件时,能查出正确的结果,但是当只提供 userName 的参数值时,userEmail 默认是 null,这就会导致 user_email=null 也成为查询条件,因而查不出正确的结果。这时就可以使用 if 标签来解决这个问题了,代码如下。

<select id="selectByUser" resultType="tk.mybatis.simple.model.SysUser">
    select id,
        user_name userName,
        user_password userPassword,
        user_email userEmail,
        user_info userInfo,
        head_img headImg,
        create_time createTime
    from sys_user
    where 1 = 1
    <if test="userName != null and userName != ''">
    and user_name like concat('%', #{userName}, '%')
    </if>
    <if test="userEmail != null and userEmail != ''">
    and user_email = #{userEmail}
    </if>
</select>

if 标签有一个必填的属性 test,test 的属性值是一个符合 OGNL 要求的判断表达式,表达式的结果可以是 true 或 false,除此之外所有的非 0 值都为 true,只有 0 为 false。为了方便理解,在表达式中,建议只用 true 或 false 作为结果。OGNL 的详细用法在 4.7 节中会有比较完整的介绍,这里暂且只关注以下内容。

  • 判断条件 property != null 或 property == null:适用于任何类型的字段,用于判断属性值是否为空。

  • 判断条件 property != '' 或 property == '':仅适用于 String 类型的字段,用于判断是否为空字符串。

  • and 和 or:当有多个判断条件时,使用 and 或 or 进行连接,嵌套的判断可以使用小括号分组,and 相当于 Java 中的与(&&),or 相当于 Java 中的或(||)。

上面两个条件的属性类型都是 String,对字符串的判断和 Java 中的判断类似,首先需要判断字段是否为 null,然后再去判断是否为空(在 OGNL 表达式中,这两个判断的顺序不会影响判断的结果,也不会有空指针异常)。在本章所有例子中,字符串的判断几乎都包含 null 和空的判断,这两个条件不是必须写在一起,可以根据实际业务决定是否需要空值判断。

有了 XML 中的方法后,还需要在 UserMapper 接口中增加对应的接口方法,代码如下。

/**
* 根据动态条件查询用户信息
*
* @param sysUser
* @return
*/
List<SysUser> selectByUser(SysUser sysUser);

测试方法如下。

@Test
public void testSelectByUser() {
    SqlSession sqlSession = getSqlSession();
    try {
        UserMapper userMapper =  sqlSession.getMapper(UserMapper.class);
        // 只查询用户名时
        SysUser user = new SysUser();
        user.setUserName("ad");
        List<SysUser> userList = userMapper.selectByUser(user);
        Assert.assertTrue(userList.size() > 0);
        // 只查询用户邮箱时
        user = new SysUser();
        user.setUserEmail("test@zccoder.com");
        userList = userMapper.selectByUser(user);
        Assert.assertTrue(userList.size() > 0);
        // 同时查询用户名和邮箱时
        user = new SysUser();
        user.setUserName("ad");
        user.setUserEmail("test@zccoder.com");
        userList = userMapper.selectByUser(user);
        // 由于没有同时符合这两个条件的用户,因此查询结果为0
        Assert.assertTrue(userList.size() == 0);
    } finally {
        // 不要忘记关闭 sqlSession
        sqlSession.close();
    }
}

上面的代码针对需求中的 3 种情况分别进行测试,输出的日志内容如下。

由于 SQL 过长,上面的日志仅保留了 from 之后的 SQL,并且查询结果只保留了条数。从日志中可以看到,查询条件的不同组合最终执行的 SQL 和预期一样,这样就实现了动态条件查询。

虽然实现了需求,但是在 XML 的方法中仍然有两个地方需要注意。

  • 注意 SQL 中 where 关键字后面的条件

    where 1=1

    由于两个条件都是动态的,所以如果没有 1=1 这个默认条件,当两个 if 判断都不满足时,最后生成的 SQL 就会以 where 结束,这样不符合 SQL 规范,因此会报错。加上 1=1 这个条件就可以避免 SQL 语法错误导致的异常。这种写法并不美观,在 4.3 节中会介绍 where 标签的用法,可以替代这种写法。

  • 注意条件中的 and(或 or)

    and user_name like concat('%',#{userName},'%')

    这里的 and(或 or)需要手动添加,当这部分条件拼接到 where 1=1 后面时仍然是合法的 SQL。因为有默认的 1=1 这个条件,我们才不需要判断第一个动态条件是否需要加上 and(或 or),因为这种情况下 and(或 or)是必须有的。

在UPDATE更新列中使用if

现在要实现这样一个需求:只更新有变化的字段。需要注意,更新的时候不能将原来有值但没有发生变化的字段更新为空或 null。通过 if 标签可以实现这种动态列更新。

先在 UserMapper 中增加接口,代码如下。

/**
 * 根据主键动态更新
 * @param sysUser 用户
 */
int updateByIdSelective(SysUser sysUser);

一般情况下,MyBatis 中选择性更新的方法名会以 Selective 作为后缀。

接下来在 UserMapper.xml 中添加对应的 SQL 语句,代码如下。

<update id="updateByIdSelective">
    update sys_user
    set
        <if test="userName != null and userName != ''">
            user_name = #{userName},
        </if>
        <if test="userPassword != null and userPassword != ''">
            user_password = #{userPassword},
        </if>
        <if test="userEmail != null and userEmail != ''">
            user_email = #{userEmail},
        </if>
        <if test="userInfo != null and userInfo != ''">
            user_info = #{userInfo},
        </if>
        <if test="headImg != null">
            head_img = #{headImg,jdbcType=BLOB},
        </if>
        <if test="createTime != null">
            create_time = #{createTime, jdbcType=TIMESTAMP},
        </if>
        id = #{id}
    where id = #{id}
</update>

这里要结合业务层的逻辑判断,确保最终产生的 SQL 语句没有语法错误。需要注意的有两点:第一点是每个 if 元素里面 SQL 语句后面的逗号;第二点就是 where 关键字前面的 id=#{id} 这个条件。以下两种情况可以帮助大家理解为什么需要关注这两点。

  • 全部的查询条件都是 null 或者空。

    如果有 id=#{id} 这个条件,最终的 SQL 如下。

    update sys_user set id=#{id} where id=#{id}

    如果没有这个条件,最终的 SQL 如下。

    update sys_user set where id=#{id}

    这个 SQL 很明显是错误的,set 关键字后面没有内容,直接是 where 关键字,不符合 SQL 语句规范。

  • 查询条件只有一个不是 null 也不是空(假设是 userName)。

    如果有 id=#{id} 这个条件,最终的 SQL 如下。

    update sys_user set user_name=#{userName},id=#{id} where id=#{id}

    如果没有这个条件,最终的 SQL 如下。

    update sys_user set user_name=#{userName},where id=#{id}

    where 关键字前面直接是一个逗号,这个 SQL 语句也是错的。

从上面两种情况来看,id=#{id} 这个条件可以最大限度保证方法不出错。除了使用这种方式外,还可以结合业务层的逻辑判断调整 XML 文件中的 SQL 来确保最终的 SQL 语句的正确性,也可以通过 where 和 set 标签来解决这些问题。

下面是以上方法的测试代码。

@Test
public void testUpdateByIdSelective() throws Exception{
    SqlSession sqlSession = getSqlSession();

    try {
        UserMapper userMapper =  sqlSession.getMapper(UserMapper.class);
        // 创建一个新的 user 对象
        SysUser user = new SysUser();
        // 更新 id = 1 的用户
        user.setId(1L);
        // 修改邮箱
        user.setUserEmail("test@zccoder.com");
        // 更新邮箱,特别注意,这里的返回值 result 执行的是 SQL 影响的行数
        int result = userMapper.updateByIdSelective(user);
        // 只更新1条数据
        Assert.assertEquals(1, result);
        // 根据当前 id 查询修改后的数据
        user = userMapper.selectById(1L);
        // 修改后的名字保存不变,但是邮箱变成了新的
        Assert.assertEquals("admin",user.getUserName());
        Assert.assertEquals("test@zccoder.com",user.getUserEmail());
    } finally {
        // 为了不影响其它测试,这里选择回滚
        sqlSession.rollback();
        // 不要忘记关闭 sqlSession
        sqlSession.close();
    }
}

该测试输出的日志代码如下。

在INSERT动态插入列中使用if

在数据库表中插入数据的时候,如果某一列的参数值不为空,就使用传入的值,如果传入参数为空,就使用数据库中的默认值(通常是空),而不使用传入的空值。使用 if 就可以实现这种动态插入列的功能。

先修改 sys_user 表,在数据库中执行如下的 SQL 语句给 user_email 列增加默认值 test@mybatis.tk

ALTER TABLE `sys_user`
MODIFY COLUMN `user_email` varchar(50) NULL DEFAULT 'test@mybatis.tk'
    COMMENT '邮箱'
    AFTER `user_password`;

下面直接修改 SysUserMapper.xml 中的 insert2 方法。

<insert id="insert2" useGeneratedKeys="true" keyProperty="id">
    insert into sys_user(
        user_name,user_password,
        <if test="userEmail != null and userEamil != ''">
            user_email,
        </if>
        user_info,head_img,create_time)
    values(
        #{userName},#{userPassword},
        <if test="userEmail != null and userEamil != ''">
            #{userEmail},
        </if>
        #{userInfo},#{headImg,jdbcType=BLOB},#{createTime,jdbcType=TIMESTAMP})
</insert>

在 INSERT 中使用时要注意,若在列的部分增加 if 条件,则 values 的部分也要增加相同的 if 条件,必须保证上下可以互相对应,完全匹配。

该方法的测试代码如下。

@Test
public void testInsert2Seclective(){
    SqlSession sqlSession = getSqlSession();
    try {
        UserMapper userMapper =  sqlSession.getMapper(UserMapper.class);
        // 创建一个user对象
        SysUser user = new SysUser();
        user.setUserName("test-selective");
        user.setUserPassword("123456");
        user.setUserInfo("test info");
        user.setCreateTime(new Date());
        // 插入数据库
        userMapper.insert2(user);
        // 获取插入的这条数据
        user = userMapper.selectById(user.getId());
        Assert.assertEquals("test@zccoder.com",user.getUserEmail());
    } finally {
        sqlSession.rollback();
        // 不要忘记关闭 sqlSession
        sqlSession.close();
    }
}

在新增的 user 中,我们并没有给 userEmail 属性赋值,这样就会使用数据库的默认值,执行测试后,输出日志如下。

观察日志输出的 SQL 语句和查询结果,INSERT 语句中并没有插入 user_email 列,查询的结果中 user_email 的值就是我们设置的默认值 test@mybatis.tk