diff --git a/repos/courses/mybatis/01-MyBatis基础/08-动态SQL.md b/repos/courses/mybatis/01-MyBatis基础/08-动态SQL.md new file mode 100644 index 000000000..f82688ed9 --- /dev/null +++ b/repos/courses/mybatis/01-MyBatis基础/08-动态SQL.md @@ -0,0 +1,672 @@ +--- +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 和 配置让自己能用起来,最后熟练使用的基础上,在空闲时尝试阅读它的源码让自己能够洞彻它的运行机制,部分问题出现的原因,同时借鉴这些技术实现来提升自己的代码高度。 + +所以在笔者的文章中,前期基本都是小白文,仅仅穿插很少量的源码研究。当然等小白文更新多了,你们还依然喜欢,后期会不定时专门对部分技术的源码进行解析。 +::: diff --git a/repos/public/img/2020/12/29/202012291143905.png b/repos/public/img/2020/12/29/202012291143905.png new file mode 100644 index 000000000..557f20e50 Binary files /dev/null and b/repos/public/img/2020/12/29/202012291143905.png differ diff --git a/repos/public/img/2020/12/29/202012291144689.png b/repos/public/img/2020/12/29/202012291144689.png new file mode 100644 index 000000000..7f5c9efcf Binary files /dev/null and b/repos/public/img/2020/12/29/202012291144689.png differ diff --git a/repos/public/img/2020/12/29/202012291144692.png b/repos/public/img/2020/12/29/202012291144692.png new file mode 100644 index 000000000..a62ae61b5 Binary files /dev/null and b/repos/public/img/2020/12/29/202012291144692.png differ diff --git a/repos/public/img/2020/12/29/202012291145084.png b/repos/public/img/2020/12/29/202012291145084.png new file mode 100644 index 000000000..64823744a Binary files /dev/null and b/repos/public/img/2020/12/29/202012291145084.png differ diff --git a/repos/public/img/2020/12/29/202012291145312.png b/repos/public/img/2020/12/29/202012291145312.png new file mode 100644 index 000000000..eb3bdf6e0 Binary files /dev/null and b/repos/public/img/2020/12/29/202012291145312.png differ