前言

C: 一晃,都来到了本系列的最后一篇了,第一篇《初识MyBatis | 快速入门》 中,我们曾烦恼的问题们(见下方代码),被 MyBatis 的强大能力逐个逐个的瓦解了。

我们还剩下没有攻克的问题,只有 动态参数拼接 了。但凡使用 JDBC 做过持久层开发,你应该就能理解根据不同条件拼接 SQL 语句有多痛苦,例如:拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号[1]、解决多余的 and等。

本篇我们要学习 MyBatis 的另一个强大特性:动态 SQL 元素,利用它,我们可以彻底摆脱这种痛苦。

// 假设BaseDao已经封装了通用CRUD操作,详情见查老师《BaseDao封装JDBC通用CRUD》public class UserDaoImpl extends BaseDao implements UserDao {    // 根据条件查询用户列表@Overridepublic List<User> findByMap(Map<String, Object> params) throws Exception {		// 动态拼接SQL语句
		StringBuffer sqlBuffer = new StringBuffer();		// 动态拼接SQL占位符参数
		List<Object> paramsList = new ArrayList<>();
		
		sqlBuffer.append(" select ");
		sqlBuffer.append("   * ");
		sqlBuffer.append(" from ");
		sqlBuffer.append("   user ");
		sqlBuffer.append(" where 1 = 1 ");		
		// 根据用户名模糊查询
		Object name = params.get("name");		if (EmptyUtils.isNotEmpty(name)) {
			sqlBuffer.append(" and name like CONCAT('%',?,'%') ");
			paramsList.add(name);
		}		
		// 根据年龄查询
		Integer age = (Integer) params.get("age");		if (EmptyUtils.isNotEmpty(age)) {
			sqlBuffer.append(" and age = ? ");
			paramsList.add(age);
		}		
		return this.selectList(sqlBuffer.toString(), paramsList.toArray(), User.class);
	}    // 保存用户@Overridepublic int save(User user) throws Exception {
		StringBuffer sqlBuffer = new StringBuffer();
        
		sqlBuffer.append(" insert into user ");
		sqlBuffer.append(" 	(name, age, email) ");
		sqlBuffer.append("  values(?, ?, ?) ");
		
		Object[] params = {user.getName(), 
                           user.getAge(),
                           user.getEmail()};        
		return this.insert(sqlBuffer.toString(), params);
	}
    
}复制代码

如果你之前用过 JSTL 或任何基于类 XML 语言的文本处理器,你对动态 SQL 元素可能会感觉似曾相识。在 MyBatis  之前的版本中,需要花时间了解大量的元素。借助功能强大的基于 OGNL 的表达式,MyBatis 3  替换了之前的大部分元素,大大精简了元素种类,现在要学习的元素种类比原来的一半还要少。[1]

  • if 判断元素
  • choose (when, otherwise) 判断元素
  • trim (where, set) 自动去除多余组成元素
  • foreach 迭代元素

上方这部分介绍,主要来自 MyBatis 官网,至于它怎么介(chui)绍(xu)自己我们不用过多关注,我们自己用了还好用才是真的。

初识MyBatis | 动态SQL | 创作者训练营_MyBatis

系列阅读

查老师有话说: 虽然查老师心中你是最棒的,但是如果你上一篇没看,还是建议打开快速串一下,以防止对下方的内容无法保持连贯性。

初识MyBatis | 动态SQL | 创作者训练营_动态SQL_02

  • 《初识MyBatis | 快速入门》
  • 《初识MyBatis | 核心对象》
  • 《初识MyBatis | 核心配置文件》
  • 《初识MyBatis | SQL映射文件之查询元素》
  • 《初识MyBatis | SQL映射文件之增删改元素》
  • 《初识MyBatis | SQL映射文件之自定义映射元素》
  • 《初识MyBatis | SQL映射文件之缓存元素》
  • 《初识MyBatis | 动态SQL》

if元素

SQL 映射文件篇的时候,我们实现过一个需求:根据用户名和年龄查询用户列表。关键实现部分如下:

public interface UserMapper {	/**
	 * 根据用户名和年龄查询
	 * @param params 条件参数
	 * @return 用户列表
	 */List<User> selectByMap(Map<String, Object> params);

}复制代码
<!-- List<User> selectByMap(Map<String, Object> params); --><!-- 在 parameterType 为 Map 集合时, #{map的键名} 可以获取集合的值 --><select id="selectByMap" parameterType="map" resultType="User">select
    	* 
    from
    	user 
    where 
    	name like concat('%', #{name}, '%')
    	and
    	age = #{age}</select>复制代码

如果一切如下方的传值测试,那什么问题也不会发生。

class TestMyBatis {    
	@Testvoid testSelectByMap() throws IOException {		// 获取SqlSession对象try (SqlSession sqlSession = MyBatisUtils.openSession()) {			// 获取 Mapper 接口
			UserMapper userMapper = sqlSession.getMapper(UserMapper.class);			// 执行 SQL
			Map<String, Object> params = new HashMap<>();
			params.put("name", "J");
			params.put("age", 20);
			List<User> userList = userMapper.selectByMap(params);			// 遍历数据
			userList.forEach(System.out::println);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
    
}复制代码

但是往往我们在做类似的需求时,对条件一般是可选的,即:name 和 age 参数可以传递值也可以不传递值,那么当你如是做之后,结果呢?我们在 Map 集合中去除掉 age 这个参数值试试。

// 执行Map<String,Object> map = new HashMap<String, Object>();
map.put("name", "J");
List<User> userList = userMapper.selectByMap(params);复制代码

控制台输出:

初识MyBatis | 动态SQL | 创作者训练营_MyBatis_03

我们去除了 age 参数值,原本的设想是不再根据 age 列进行条件筛选,但是它还是处于生效状态,因为我们并没有添加 SQL 的条件判断, age = null 的结果显而易见是不存在的。


我们来使用 if 元素改造一下吧。

<!--
 	if元素 用于条件判断
		test属性:利用关系和逻辑运算符编写条件,不需要用${}括起来
		注意:有些同学受到 JSTL 荼毒太深,就容易犯错 --><select id="selectByMap" parameterType="map" resultType="User">
	select
		*
	from
		user
	where
		name like concat('%', #{name},'%')		<if test="age != null">
			and age = #{age}		</if></select>复制代码

执行刚才的测试代码,此次就没有任何问题了。

初识MyBatis | 动态SQL | 创作者训练营_动态SQL_04

查老师有话说: if 元素可以说是日常开发用的最多的了,而且又简单,又好用。

where元素

有了 if 元素的加持,如果我们只是要进行 age 这一项条件查询,那就去掉 Map 集合中的 name 参数,这时候似乎只需要在 SQL 映射文件中也给 name 条件查询加上个判断即可。

<!-- 字符串判断可以多加一个 != '' 空字符串的判断 --><select id="selectByMap" parameterType="map" resultType="User">
	select
		*
	from
		user
	where		<if test="name != null and name != ''">
			name like concat('%', #{name},'%')		</if><if test="age != null">
			and age = #{age}		</if></select>复制代码
// 执行Map<String,Object> map = new HashMap<String, Object>();
map.put("age", 20);
List<User> userList = userMapper.selectByMap(map);		
复制代码

但结果并不是如此,当测试执行后,控制台报错了。

初识MyBatis | 动态SQL | 创作者训练营_动态SQL_05

但实际上,你就算不运行,你好好看看上方的 SQL 部分,就可以看出问题,它一共有可能出现4种情况。

  1. name 和 age 都传递了值,那一切都正常执行
  2. 只传递了 name 值,那么也正常
  3. 如果只传递了 age 值,那么就会多余一个 and 出现
  4. 如果都没有传值,那么就会多余一个 where 出现

在之前 JDBC 开发中,我们的解决方法是添加一个 where 1 = 1 的恒等式。

<select id="selectByMap" parameterType="map" resultType="User">
	select
		*
	from
		user
	where
		1 = 1		<if test="name != null and name != ''">
			and name like concat('%', #{name},'%')		</if><if test="age != null">
			and age = #{age}		</if></select>复制代码

在 MyBatis 中,准备了一个专门的元素用来解决此类问题,比添加额外的 1=1 语句更简单。

<!-- 
	where 元素在执行时,会智能添加一个 where 查询关键字
	并且结合着 if 元素,它会自动【去除】每个 if 段内前面【多余的 and 或 or】 关键字(不会去除后面的)
 --><select id="selectByMap" parameterType="map" resultType="User">
	select
		*
	from
		user	<where><if test="name != null and name != ''">
			name like concat('%', #{name},'%')		</if><if test="age != null">
			and age = #{age}		</if></where></select>复制代码

set元素

set 元素,和 where 元素一样也是结合 if 元素用于去除多余内容的。顾名思义,它是和 update 语句的 set 部分有关的。

我们来做一个修改示例:

public interface UserMapper {	/**
	 * 修改用户信息
	 * @param user 用户信息
	 * @return 影响行数
	 */int update(User user);

}复制代码
<!-- int update(User user); --><update id="update" parameterType="User">
	update
		user
	set
		name = #{name},
		age = #{age},
		email = #{email},
		roleId = #{roleId}
	where
		id = #{id}</update>复制代码
class TestMyBatis {	@Testvoid testUpdate1() {		// 获取SqlSessiontry (SqlSession sqlSession = MyBatisUtils.openSession()) {			
			// 获取Mapper
			UserMapper userMapper = sqlSession.getMapper(UserMapper.class);// 只想修改1号用户的名字
			User user = new User();
			user.setName("Charles");
			user.setId(1L);			// 执行int rows = userMapper.update(user);
			System.out.println("影响行数为:" + rows);			
			// 提交事务
			sqlSession.commit();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}

}复制代码

执行测试后,本来我们只想修改用户名,结果却是将所有的列都清空了。

初识MyBatis | 动态SQL | 创作者训练营_MyBatis_06

初识MyBatis | 动态SQL | 创作者训练营_动态SQL_07

所以它也需要加上 if 元素来判断是否需要修改。

<update id="update" parameterType="User">
	update
		user
	set		<if test="name != null">
			name = #{name},		</if><if test="age != null">
			age = #{age},		</if><if test="email != null">
			email = #{email},		</if><if test="roleId != null">
			roleId = #{roleId}		</if>
	where
		id = #{id}</update>复制代码

但如果仅仅这样,当最后一个条件判断不成立时,那么前面的内容只要有一个传了值就会在结尾出现多余 , 的问题,使用 set 元素可以很容易解决该类问题。

<update id="update" parameterType="User">
	update
		user	<set><if test="name != null">
			name = #{name},		</if><if test="age != null">
			age = #{age},		</if><if test="nemailame != null">
			email = #{email},		</if><if test="roleId != null">
			roleId = #{roleId}		</if></set>
	where
		id = #{id}</update>复制代码

set 元素可以智能添加一个 set 关键字,并且可以去除 if 段后面多余的 , 号。

trim元素

当我们需要灵活变更自动去除的内容时,where 和 set 就不合适了,它们是死板的,只能去除那几样。

trim 元素,看名字就知道是用来去除东西,下面我们用 trim 元素分别实现一下刚才的 where 元素和 set元素。

实现 where 元素。

<!-- 
	trim元素,用于去除多余内容
		prefix:为整段内容添加一个前缀
		prefixOverrides:自动去除每个 if 段的多余前缀内容
 --><select id="selectByMap" parameterType="map" resultType="User">
	select
		*
	from
		user	<trim prefix="where" prefixOverrides="and | or"><if test="name != null and name != ''">
			name like concat('%', #{name},'%')		</if><if test="age != null">
			and age = #{age}		</if></trim></select>复制代码

实现 set 元素。

<!-- 
	trim元素,用于去除多余内容
		suffix:为整段内容添加一个后缀
		suffixOverrides:去除每个if段后多余的内容
 --><update id="update" parameterType="User">
	update
		user	<trim prefix="set" suffix="where id = #{id}" suffixOverrides=","><if test="name != null">
			name = #{name},		</if><if test="age != null">
			age = #{age},		</if><if test="email != null">
			email = #{email},		</if><if test="roleId != null">
			roleId = #{roleId}		</if></trim></update>复制代码

foreach元素

上方的四个元素,可以说在 SQL 映射文件中,用的最多,你把它们掌握了一般情况够用了。

但是有时候我们有一些特殊的需求,例如:下方的案例,根据角色 id 的集合来查询用户列表。

public interface UserMapper {	/**
	 * 根据角色集合查询用户
	 * @param roleIds 角色集合
	 * @return 用户列表
	 */List<User> selectByRoleList(List<Long> roleIds);
    
}复制代码
<!-- 
	foreach 迭代元素
		collection:
			可选值为 list(List集合)、array(数组)、Map集合的键(对应的值需要为List或Array)
		item:遍历出来的每一项
		open:在整段前添加的内容
		separator:遍历出来的每一项都添加什么分隔符
		close:在整段后添加的内容
 --><select id="selectByRoleList" parameterType="long" resultType="User">
	select
		*
	from
		user
	where
		roleId in 
		<foreach collection="list" item="id" open="(" separator="," close=")">
			#{id}		</foreach></select>复制代码

测试一下:

class TestMyBatis {	@Testvoid selectByRoleList() {		// 获取SqlSessiontry (SqlSession sqlSession = MyBatisUtils.openSession()) {			
			// 获取Mapper
			UserMapper userMapper = sqlSession.getMapper(UserMapper.class);			
			// 执行
			List<Long> roleIds = Arrays.asList(1L, 2L);
			List<User> userList = userMapper.selectByRoleList(roleIds);			
			// 遍历
			userList.forEach(System.out::println);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}复制代码

控制台输出:

-- 输出的 SQLselect * from user where roleId in ( ? , ? )复制代码
User [id=2, name=JSON, age=20, email=Jack@126.com]
User [id=3, name=Tom, age=28, email=Tom@126.com]
User [id=4, name=Sandy, age=21, email=Sandy@126.com]
User [id=5, name=Billie, age=24, email=Billie@126.com]
User [id=8, name=PeiQi, age=18, email=PeiQi@126.com]复制代码

choose元素

最后我们再介绍一个应用不算太多的元素:choose,它比较类似于在 Java 中的 switch 选择结构。

案例需求:查询用户列表,如果参数 name 有值那就根据 name 模糊查询,否则参数 age 有值那就根据 age 查询, 如果都没值那就查询所有角色为1的用户。

public interface UserMapper {	/**
	 * 查询用户列表
	 * 	name有值时,根据名字做模糊查询
	 * 	否则age有值时,根据年龄等值查询
	 * 	都没值,查询角色Id为1的用户
	 * @param user 查询条件
	 * @return 用户列表
	 */List<User> selectByUser(User user);

}复制代码
<select id="selectByUser" parameterType="User" resultType="User">
	select
		*
	from
		user	<where><choose><when test="name != null and name != ''">
				name like concat('%', #{name},'%')			</when><when test="age != null">
				age = #{age}			</when><otherwise>
				roleId = 1			</otherwise></choose></where></select>复制代码

测试一下,如果同时传了 name 和 age的值。

@Testvoid testSelectByUser() {try (SqlSession sqlSession = MyBatisUtils.openSession()){

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        User user = new User();
        user.setName("T");
        user.setAge(18);
        List<User> userList = userMapper.selectByUser(user);

        userList.forEach(System.out::println);
    } catch (Exception e) {
        e.printStackTrace();
    }
}复制代码

控制台输出:

-- 输出的 SQLselect * from user WHERE name like concat('%', ?,'%')复制代码

测试一下,如果只传了 age 的值。

@Testvoid testSelectByUser() {try (SqlSession sqlSession = MyBatisUtils.openSession()){

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        User user = new User();
        user.setAge(18);
        List<User> userList = userMapper.selectByUser(user);

        userList.forEach(System.out::println);
    } catch (Exception e) {
        e.printStackTrace();
    }
}复制代码

控制台输出:

-- 输出的 SQLselect * from user WHERE age = ?复制代码

测试一下,如果什么值都没传。

@Testvoid testSelectByUser() {try (SqlSession sqlSession = MyBatisUtils.openSession()){

        UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

        List<User> userList = userMapper.selectByUser(null);

        userList.forEach(System.out::println);
    } catch (Exception e) {
        e.printStackTrace();
    }
}复制代码

控制台输出:

-- 输出的 SQLselect * from user WHERE roleId = 1复制代码

查老师有话说: 效果很明显了,自上而下进行条件判断,只要其中一个满足,后面的条件就不再执行。这就是 switch 的机制。

后记

《初识MyBatis》 系列到本篇就结束了,查老师的更文速度,你还能追的上吗?:smile:

其实这不过是因为查老师有存稿而已,否则哪那么快,每篇文章在最初时都花费了大量的时间,在存稿基础上再来发表又花费了不短时间,所以真没什么产出速度,大概比一些"临近太监的网文"的作者快点?

这个系列结束,不代表 MyBatis 系列就完全结束了,后续查老师会再次抽时间开放 MyBatis 新的系列 《MyBatis原理》,当然这可能需要一段时间,因为查老师最近在忙另一个系列。但愿那时候还在关注的你,依然保持有对 MyBatis 热爱、研究的心。:gift_heart:

查老师有话说: 对于技术的学习,查老师一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。

所以在查老师的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。