--- title: 动态SQL author: 查尔斯 date: 2020/12/29 13:28 categories: - MyBatis快速入门 tags: - MyBatis - ORM框架 --- # 动态SQL ## 前言 **C:** 一晃,都来到了本系列的最后一篇了,第一篇[《快速入门》](./01-快速入门) 中,我们曾烦恼的问题们(见下方代码),被 MyBatis 的强大能力逐个逐个的瓦解了。 我们还剩下没有攻克的问题,只有 **动态参数拼接** 了。但凡使用 JDBC 做过持久层开发,你应该就能理解根据不同条件拼接 SQL 语句有多痛苦,例如:拼接时要确保不能忘记添加必要的空格,还要注意去掉列表最后一个列名的逗号[1]、解决多余的 and等。 本篇我们要学习 MyBatis 的另一个强大特性:动态 SQL 元素,利用它,我们可以彻底摆脱这种痛苦。 ```java // 假设BaseDao已经封装了通用CRUD操作,详情见笔者《BaseDao封装JDBC通用CRUD》 public class UserDaoImpl extends BaseDao implements UserDao { // 根据条件查询用户列表 @Override public List findByMap(Map params) throws Exception { // 动态拼接SQL语句 StringBuffer sqlBuffer = new StringBuffer(); // 动态拼接SQL占位符参数 List 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); } // 保存用户 @Override public 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)自己我们不用过多关注,我们自己用了还好用才是真的。 ## if元素 SQL 映射文件篇的时候,我们实现过一个需求:根据用户名和年龄查询用户列表。关键实现部分如下: ```java public interface UserMapper { /** * 根据用户名和年龄查询 * @param params 条件参数 * @return 用户列表 */ List selectByMap(Map params); } ``` ```xml ``` 如果一切如下方的传值测试,那什么问题也不会发生。 ```java class TestMyBatis { @Test void testSelectByMap() throws IOException { // 获取SqlSession对象 try (SqlSession sqlSession = MyBatisUtils.openSession()) { // 获取 Mapper 接口 UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 执行 SQL Map params = new HashMap<>(); params.put("name", "J"); params.put("age", 20); List userList = userMapper.selectByMap(params); // 遍历数据 userList.forEach(System.out::println); } catch (Exception e) { e.printStackTrace(); } } } ``` 但是往往我们在做类似的需求时,对条件一般是可选的,即:name 和 age 参数可以传递值也可以不传递值,那么当你如是做之后,结果呢?我们在 Map 集合中去除掉 age 这个参数值试试。 ```java // 执行 Map map = new HashMap(); map.put("name", "J"); List userList = userMapper.selectByMap(params); ``` **控制台输出:** ![202012291143905](../../../public/img/2020/12/29/202012291143905.png) 我们去除了 age 参数值,原本的设想是不再根据 age 列进行条件筛选,但是它还是处于生效状态,因为我们并没有添加 SQL 的条件判断, age = null 的结果显而易见是不存在的。 --- 我们来使用 if 元素改造一下吧。 ```xml ``` 执行刚才的测试代码,此次就没有任何问题了。 ![202012291144689](../../../public/img/2020/12/29/202012291144689.png) ::: tip 笔者说 if 元素可以说是日常开发用的最多的了,而且又简单,又好用。 ::: ## where元素 有了 if 元素的加持,如果我们只是要进行 age 这一项条件查询,那就去掉 Map 集合中的 name 参数,这时候似乎只需要在 SQL 映射文件中也给 name 条件查询加上个判断即可。 ```xml ``` ```java // 执行 Map map = new HashMap(); map.put("age", 20); List userList = userMapper.selectByMap(map); ``` 但结果并不是如此,当测试执行后,控制台报错了。 ![202012291144692](../../../public/img/2020/12/29/202012291144692.png) 但实际上,你就算不运行,你好好看看上方的 SQL 部分,就可以看出问题,它一共有可能出现4种情况。 1. name 和 age 都传递了值,那一切都正常执行 2. 只传递了 name 值,那么也正常 3. *如果只传递了 age 值,那么就会多余一个 and 出现* 4. *如果都没有传值,那么就会多余一个 where 出现* 在之前 JDBC 开发中,我们的解决方法是添加一个 `where 1 = 1` 的恒等式。 ```xml ``` 在 MyBatis 中,准备了一个专门的元素用来解决此类问题,比添加额外的 `1=1` 语句更简单。 ```xml ``` ## set元素 set 元素,和 where 元素一样也是结合 if 元素用于去除多余内容的。顾名思义,它是和 update 语句的 set 部分有关的。 我们来做一个修改示例: ```java public interface UserMapper { /** * 修改用户信息 * @param user 用户信息 * @return 影响行数 */ int update(User user); } ``` ```xml update user set name = #{name}, age = #{age}, email = #{email}, roleId = #{roleId} where id = #{id} ``` ```java class TestMyBatis { @Test void testUpdate1() { // 获取SqlSession try (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(); } } } ``` 执行测试后,本来我们只想修改用户名,结果却是将所有的列都清空了。 ![202012291145084](../../../public/img/2020/12/29/202012291145084.png) ![202012291145312](../../../public/img/2020/12/29/202012291145312.png) 所以它也需要加上 if 元素来判断是否需要修改。 ```xml update user set name = #{name}, age = #{age}, email = #{email}, roleId = #{roleId} where id = #{id} ``` 但如果仅仅这样,当最后一个条件判断不成立时,那么前面的内容只要有一个传了值就会在结尾出现多余 `,` 的问题,使用 set 元素可以很容易解决该类问题。 ```xml update user name = #{name}, age = #{age}, email = #{email}, roleId = #{roleId} where id = #{id} ``` set 元素可以智能添加一个 set 关键字,并且可以去除 if 段后面多余的 `,` 号。 ## trim元素 当我们需要灵活变更自动去除的内容时,where 和 set 就不合适了,它们是死板的,只能去除那几样。 trim 元素,看名字就知道是用来去除东西,下面我们用 trim 元素分别实现一下刚才的 where 元素和 set元素。 实现 where 元素。 ```xml ``` 实现 set 元素。 ```xml update user name = #{name}, age = #{age}, email = #{email}, roleId = #{roleId} ``` ## foreach元素 上方的四个元素,可以说在 SQL 映射文件中,用的最多,你把它们掌握了一般情况够用了。 但是有时候我们有一些特殊的需求,例如:下方的案例,根据角色 id 的集合来查询用户列表。 ```java public interface UserMapper { /** * 根据角色集合查询用户 * @param roleIds 角色集合 * @return 用户列表 */ List selectByRoleList(List roleIds); } ``` ```xml ``` 测试一下: ```java class TestMyBatis { @Test void selectByRoleList() { // 获取SqlSession try (SqlSession sqlSession = MyBatisUtils.openSession()) { // 获取Mapper UserMapper userMapper = sqlSession.getMapper(UserMapper.class); // 执行 List roleIds = Arrays.asList(1L, 2L); List userList = userMapper.selectByRoleList(roleIds); // 遍历 userList.forEach(System.out::println); } catch (Exception e) { e.printStackTrace(); } } } ``` **控制台输出:** ```sql -- 输出的 SQL select * 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的用户。** ```java public interface UserMapper { /** * 查询用户列表 * name有值时,根据名字做模糊查询 * 否则age有值时,根据年龄等值查询 * 都没值,查询角色Id为1的用户 * @param user 查询条件 * @return 用户列表 */ List selectByUser(User user); } ``` ```xml ``` 测试一下,如果同时传了 name 和 age的值。 ```java @Test void testSelectByUser() { try (SqlSession sqlSession = MyBatisUtils.openSession()){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = new User(); user.setName("T"); user.setAge(18); List userList = userMapper.selectByUser(user); userList.forEach(System.out::println); } catch (Exception e) { e.printStackTrace(); } } ``` **控制台输出:** ```sql -- 输出的 SQL select * from user WHERE name like concat('%', ?,'%') ``` 测试一下,如果只传了 age 的值。 ```java @Test void testSelectByUser() { try (SqlSession sqlSession = MyBatisUtils.openSession()){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); User user = new User(); user.setAge(18); List userList = userMapper.selectByUser(user); userList.forEach(System.out::println); } catch (Exception e) { e.printStackTrace(); } } ``` **控制台输出:** ```sql -- 输出的 SQL select * from user WHERE age = ? ``` 测试一下,如果什么值都没传。 ```java @Test void testSelectByUser() { try (SqlSession sqlSession = MyBatisUtils.openSession()){ UserMapper userMapper = sqlSession.getMapper(UserMapper.class); List userList = userMapper.selectByUser(null); userList.forEach(System.out::println); } catch (Exception e) { e.printStackTrace(); } } ``` **控制台输出:** ```sql -- 输出的 SQL select * from user WHERE roleId = 1 ``` ::: tip 笔者说 效果很明显了,自上而下进行条件判断,只要其中一个满足,后面的条件就不再执行。这就是 switch 的机制。 ::: ## 参考文献 [1]MyBatis 官网. 动态 SQL[EB/OL]. https://mybatis.org/mybatis-3/zh/dynamic-sql.html. 2020-12-28 ## 后记 《初识MyBatis》 系列到本篇就结束了,笔者的更文速度,你还能追的上吗?:smile: 其实这不过是因为笔者有存稿而已,否则哪那么快,每篇文章在最初时都花费了大量的时间,在存稿基础上再来发表又花费了不短时间,所以真没什么产出速度,大概比一些"临近太监的网文"的作者快点? 这个系列结束,不代表 MyBatis 系列就完全结束了,后续笔者会再次抽时间开放 MyBatis 新的系列 《MyBatis原理》,当然这可能需要一段时间,因为笔者最近在忙另一个系列。但愿那时候还在关注的你,依然保持有对 MyBatis 热爱、研究的心。:gift_heart: ::: info 笔者说 对于技术的学习,笔者一贯遵循的步骤是:先用最最简单的 demo 让它跑起来,然后学学它的最最常用 API 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。 所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。 :::