国庆节10月1日
--


给员工列表加上姓名、性别、入职日期范围三个查询条件——控制层怎么接收日期参数、SQL 为什么搬进 XML、条件写死时「用户不填条件就查不出数据」的问题,以及用 if 与 where 标签把 SQL 改成动态的完整写法,最后把六个请求参数封装成 EmpQueryParam
.webp)
67 篇把分页做出来了,但页面顶部的搜索栏还没人管:用户填了”姓名=张、性别=男、入职时间 2007-09-01 到 2022-09-01”,接口得照着这些条件去查。这一篇(PPT 第 54-65 页)要解决的就是”条件 + 分页”这两件事怎么合到一起。
PPT 把这一节拆成了四段,本篇也按这个顺序走:
| PPT 页 | 讲什么 |
|---|---|
| 54-58 | 条件分页查询的分析与基本实现(控制层收条件、SQL 写进 XML) |
| 59-61 | 优化一:请求参数太多 → 封装成一个对象接收 |
| 62-63 | 优化二:条件写死了 → 改成动态 SQL |
| 64 | 必答问答(动态 SQL 的使用场景、<if> 与 <where> 的作用) |
PPT 第 54 页是本节的目录页(准备工作 / 分页查询 / 条件分页查询),第 55 页写着一句”已实现”——已实现的是分页(67 篇),本节要补的是条件。
第 56 页把三层职责重新梳了一遍,和 67 篇的原始方式对比,多了”条件”两个字:
| 层 | 职责(PPT 原文) |
|---|---|
| Controller | 接收参数(分页、条件);调用 Service 进行分页查询、获取 PageResult;响应结果 |
| Service | 使用 PageHelper 完成分页条件查询;封装 PageResult 对象并返回 |
| Mapper | SQL:select ... from emp e ... where e.name like ? and e.gender = ? and e.entry_date between ? and ? order by ... |
请求样例也变长了,条件参数挂在分页参数前面:
1/emps?name=张&gender=1&begin=2007-09-01&end=2022-09-01这四个参数正好对应页面上搜索栏的三样东西:姓名(文本框)、性别(下拉框)、入职时间(开始日期 + 结束日期)。接口文档 2.1 里那四个”否”(name/gender/begin/end)就是它们——都不是必须的,这一点很关键,下面”优化二”会看到它带来的麻烦。
PPT 第 57 页给了控制层的代码:
1@GetMapping2public Result page(@RequestParam(defaultValue = "1") Integer page,3 @RequestParam(defaultValue = "10") Integer pageSize,4 String name, Integer gender,5 @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,6 @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){7 log.info("分页查询,参数:{},{},{},{},{},{}", page, pageSize, name, gender, begin, end);8 PageResult<Emp> pageResult = empService.page(page, pageSize, name, gender, begin, end);9 return Result.success(pageResult);10}六个参数分成两组:
page、pageSize,还是老样子用 @RequestParam(defaultValue = …) 给默认值;name(字符串)、gender(整数)、begin/end(入职日期范围)。前两个条件参数没有加任何注解——33 篇讲过,简单类型参数只要形参名和 queryString 里的参数名一致就能自动接收(?name=张 对 String name)。
麻烦的是两个日期参数。PPT 在这一页下面专门提了一句:
日期时间类型参数接收时,需要通过
@DateTimeFormat注解指定前端传递的日期格式。
原因是 begin/end 的类型是 LocalDate(Java 的日期类型),而前端在 URL 上传来的是字符串 2007-09-01。Spring 默认不知道该按哪种格式把这个字符串转成 LocalDate,所以要靠 @DateTimeFormat(pattern = "yyyy-MM-dd") 明确告诉它:“按 yyyy-MM-dd 这个格式解析”。
格式必须和前端发过来的对得上。注解里写 yyyy-MM-dd,前端就得传 2007-09-01 这种形式;如果前端传的是 2007/09/01 或 2007年09月01日,字符串解析不了,请求会直接失败(参数类型转换异常)。接口文档里 begin/end 的示例值写的就是 2010-01-01,所以后端按 yyyy-MM-dd 收——联调时日期参数报错,第一件事就是核对前后端的日期格式。
条件参数一多,SQL 也跟着变长了。PPT 第 57 页的数据访问层分成两个文件:
1// EmpMapper.java:只声明方法,不写 SQL2public List<Emp> list(String name, Integer gender, LocalDate begin, LocalDate end);1<!-- EmpMapper.xml:写 SQL -->2<select id="list" resultType="com.itheima.pojo.Emp">3 select e.*, d.name as deptName from emp e left join dept d on e.dept_id = d.id4 where e.name like concat('%',#{name},'%') and e.gender = #{gender} and e.entry_date between #{begin} and #{end}5</select>这就是 55 篇学的 XML 映射配置,三条默认规则照旧要守(文件与接口同包同名:接口在 src/main/java/com/itheima/mapper/EmpMapper.java,XML 放在 src/main/resources/com/itheima/mapper/EmpMapper.xml;namespace 是接口全限定名;语句 id 是方法名、resultType 写单条记录的类型)。
为什么这里用 XML 而不是 @Select 注解? 55 篇给过判断标准:简单 SQL 用注解就够,复杂 SQL(长、要拼接、要动态变化)用 XML。这条 SQL 又长又要连表,而且下一步还要改成”条件可变”的动态 SQL——在注解里用字符串拼 SQL 简直是灾难,所以它必须进 XML。
两处写法说明:
#{} 里写的是接口方法的形参名(name、gender、begin、end)。方法有多个参数时,#{} 就按形参名取值——这是 50 篇讲的预编译占位符,能防 SQL 注入;like 的写法是 concat('%',#{name},'%'):把百分号拼在参数两侧,实现”包含”匹配(用户输入”张”,就能匹配到”张无忌”)。| PPT 的问题 | 答案 |
|---|---|
@RequestParam 注解的使用场景? | ① 接收请求参数,参数名不一致时(形参名和 queryString 的 key 不同,用 @RequestParam("前端参数名") 对上);② 设置请求参数的默认值(@RequestParam(defaultValue = "1")——分页的 page/pageSize 就靠它) |
@DateTimeFormat 的使用场景? | 用于接收日期时间类型的参数,指定日期格式——写法 @DateTimeFormat(pattern = "yyyy-MM-dd"),配合 LocalDate 使用;不写它,2007-09-01 这样的字符串转不成日期对象 |
第 59 页接着往下推:接口已经要收 6 个参数了,PPT 把请求参数和那段控制层代码又摆了一遍,问的其实是同一个问题——方法签名太长了。
PPT 第 60 页把理由说得很清楚:
如果 controller 方法的参数较多,且未来可能继续增加,这会使得方法签名变得复杂难以维护,此时可以考虑将多个请求参数封装为一个对象。
想一想也是:员工列表要 6 个参数,以后按”职位""薪资范围”查还要再加;参数一个个排在方法签名上,方法头比方法体还长,调 Service 时还要一个个按顺序传——顺序传错一个就是隐藏的 bug。
PPT 第 60 页给的方案是一个专门装”查询参数”的类:
1@Data2public class EmpQueryParam {3 private Integer page = 1; // 当前页码4 private Integer pageSize = 10; // 每页记录数5 private String name; // 员工姓名6 private Integer gender; // 员工性别7 @DateTimeFormat(pattern = "yyyy-MM-dd")8 private LocalDate begin; // 入职日期9 @DateTimeFormat(pattern = "yyyy-MM-dd")10 private LocalDate end; // 入职日期11}控制层随之简化成”一个参数”:
1@GetMapping2public Result page(EmpQueryParam empQueryParam){3 log.info("分页查询,参数:{}", empQueryParam);4 PageResult<Emp> pageResult = empService.page(empQueryParam);5 return Result.success(pageResult);6}对照改造前的版本,有三处变化值得注意:
page(EmpQueryParam empQueryParam)——以后加条件只在类里加字段,方法签名不动;page/pageSize 的默认值从 @RequestParam(defaultValue = …) 挪到了字段的初始值(private Integer page = 1;)——对象接收时没有 defaultValue 可用,就在字段上直接给默认值,效果一样(不传就是 1 和 10);@DateTimeFormat 也搬家了:日期格式注解挪到字段上,照样生效。为什么 Spring 能把 URL 上的参数装进这个对象? 因为 EmpQueryParam 是一个普通的 POJO,里面的属性名(page、pageSize、name、gender、begin、end)和 queryString 的 key 一一对应,Spring MVC 会自动按名字把参数值塞进对应的属性里——33 篇讲的”POJO 参数”就是这种用法。
Service 那边的签名也跟着简化(page(EmpQueryParam empQueryParam)),实现里把分页参数从对象里取出来:
1@Override2public PageResult<Emp> page(EmpQueryParam empQueryParam) {3 //1. 设置分页参数(PageHelper)4 PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());5
6 //2. 执行查询7 List<Emp> empList = empMapper.list(empQueryParam);8
9 //3. 解析查询结果,并封装10 Page<Emp> p = (Page<Emp>) empList;11 return new PageResult<Emp>(p.getTotal(), p.getResult());12}(数据访问层的方法也变成 List<Emp> list(EmpQueryParam empQueryParam);——SQL 里要用哪个条件,就从对象的属性里取,见下一节。)
| PPT 的问题 | 答案 |
|---|---|
queryString 查询参数的接收方式(/emps?name=xxx&gender=xxx)? | 两种:参数较少时,在 controller 方法中定义一个一个形参接收;参数较多时,在 controller 方法中定义一个对象接收(属性名与参数名对应) |
参数接收顺了,但 SQL 里还埋着一个大问题。PPT 第 62 页把 XML 里那条 SQL 又贴了一遍,然后在旁边列出四种不同的查询需求:
1select * from emp e ... where e.name like ?2select * from emp e ... where e.gender = ?3select * from emp e ... where e.name like ? and e.gender = ?4select * from emp e ... where e.gender = ? and entry_date between ? and ?它们对应页面上的真实操作:用户只填姓名、只选性别、姓名和性别都填、只选性别 + 入职时间范围……PPT 的结论只有五个字:
条件写死了
也就是说,现在 XML 里那条 SQL 的条件是固定三个(姓名 + 性别 + 日期范围):
1where e.name like concat('%',#{name},'%') and e.gender = #{gender} and e.entry_date between #{begin} and #{end}只要用户有一项没填,那一项参数就是 null,而 SQL 里 e.gender = null 这种比较永远不成立(null 参与比较的结果还是 null,不是”真”)——结果是一行都查不出来。要支持四种、八种、十六种条件组合,难道要写十六个查询方法?显然不行。PPT 把真正的需求写在第 62 页下面:
查询条件随着用户输入的条件变化而变化

PPT 第 63 页给出了定义和两个标签:
随着用户的输入或外部条件的变化而变化的 SQL 语句,我们称为动态 SQL。
<if>:判断条件是否成立,如果条件为 true,则拼接 SQL。<where>:根据查询条件,来生成 where 关键字,并会自动去除条件前面多余的 and 或 or。
两个标签各管一件事,配合起来正好解决”条件个数不固定”:
| 标签 | 作用 | 解决的问题 |
|---|---|---|
<if test="条件"> | 条件成立才把里面的 SQL 片段拼进去 | 用户填了哪个条件,才拼哪个条件 |
<where> | 有内容时才生成 where 关键字,并去掉开头多余的 and/or | 第一个条件前面不用纠结写不写 and;一个条件都没有时,连 where 都不生成 |
PPT 第 63 页给了一小段示例,重点是那个 test 属性:
1<if test="gender != null">2 and e.gender = #{gender}3</if>test 里写的是判断表达式(gender != null 表示”性别这个参数有值”)——有值才拼 and e.gender = ?,没值就整段跳过。
把 55 篇的三条规则和上面两个标签合起来,EmpMapper.xml 的最终写法是(课程 05. 最终代码 里就是这个):
1<?xml version="1.0" encoding="UTF-8" ?>2<!DOCTYPE mapper3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"4 "https://mybatis.org/dtd/mybatis-3-mapper.dtd">5<mapper namespace="com.itheima.mapper.EmpMapper">6
7 <select id="list" resultType="com.itheima.pojo.Emp">8 select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id9 <where>10 <if test="name != null and name != ''">11 e.name like concat('%',#{name},'%')12 </if>13 <if test="gender != null">14 and e.gender = #{gender}15 </if>16 <if test="begin != null and end != null">17 and e.entry_date between #{begin} and #{end}18 </if>19 </where>20 order by e.update_time desc21 </select>22
23</mapper>逐条对照着看:
| 写法 | 为什么这么写 |
|---|---|
<if test="name != null and name != ''"> | 姓名是字符串,空字符串也算”没填”,所以要同时判断 != null 和 != '' |
<if test="gender != null"> | 性别是数字,判断不为 null 就够 |
<if test="begin != null and end != null"> | 入职日期是范围,两个都有值才拼 between … and …(只填一个的话这个条件就不生效) |
第一个 <if> 里不写 and,后两个写 and | 无论哪个条件先被拼进来,<where> 都会把开头的 and/or 去掉——所以第一个写不写都行,课程代码选择不写 |
<where> 里一个条件都没成立 | 生成出来的 SQL 里连 where 关键字都没有,就是”查全部” |
order by e.update_time desc 放在 <where> 外面 | 排序是固定的,不属于”可变条件” |
本机实测(工程跑起来看服务端日志,同一套动态 SQL 发出两条完全不同的语句):
1# 带姓名 + 性别条件(/emps?name=李&gender=1&page=1&pageSize=5)2==> Preparing: SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id WHERE e.name LIKE concat('%', ?, '%') AND e.gender = ?3==> Preparing: select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id4 WHERE e.name like concat('%', ?, '%') and e.gender = ? order by e.update_time desc LIMIT ?5
6# 一个条件都不带(/emps?page=2&pageSize=5)7==> Preparing: SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id8==> Preparing: select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id9 order by e.update_time desc LIMIT ?, ?对比这两组日志就是动态 SQL 最有力的证据:
WHERE 后面只拼了用户填的那两个条件(name like 和 gender =),没填的日期条件根本没出现——不是拼成了 entry_date between null and null,而是整段消失;where 关键字都没有(… d.id order by …),直接查全部——这就是 <where> 标签”没有条件就不生成 where”的效果,也是为什么”无条件查询”能正常返回 30 条;LIMIT 分别是 LIMIT ?(第 1 页、每页 5 条)和 LIMIT ?, ?(第 2 页、参数 5, 5)——分页那部分还是 67 篇的 PageHelper 在管,和动态 SQL 互不干扰。本机实测(条件分页查出来的数):GET /emps?begin=2007-09-01&end=2022-09-01&page=1&pageSize=5 返回 total 为 20、rows 5 条——只用了日期一个条件;而同一个接口不带条件时 total 是 30。同一个方法、同一段 XML,条件变了结果就跟着变,不需要写第二个查询方法。
条件参数里有中文(姓名),联调时特别容易踩一个坑,顺手记在这里:
本机实测:在 Windows 命令行里用 curl --get --data-urlencode "name=张" "http://localhost:8080/emps" 发请求,服务端日志里收到的参数是 ??、查询结果 total 是 0(查不到数据)。原因是 Windows 命令行把中文按 GBK 编码发出去,服务端按 UTF-8 解析就变成了乱码。
两个正确做法:
?name=%E6%9D%8E(查”李”)→ 正常返回 total 6 条;结论:“接口查不到数据”时,先看服务端日志里收到的参数对不对,别急着怀疑 SQL。
| PPT 的问题 | 答案 |
|---|---|
| Mybatis 中动态 SQL 的使用场景? | 如果 SQL 语句是不固定的,是随着用户的输入或外部条件的变化而变化的——就比如本节的条件分页查询:用户填几个条件、填哪几个都不一定 |
MyBatis 中动态 SQL 的 <if> 及 <where> 标签的作用? | <if>:条件判断,如果条件成立,则拼接对应的 SQL 片段;<where>:根据查询条件来生成 where 关键字,并会自动去除条件前面多余的 and 或 or |
| 问题 | 答案 |
|---|---|
| 条件分页查询要接哪几个条件? | name(姓名)、gender(性别)、begin/end(入职日期范围),都不必须 |
| 日期参数怎么接收? | @DateTimeFormat(pattern = "yyyy-MM-dd") + LocalDate——指定前端传来的日期格式;格式对不上会转换失败 |
| SQL 为什么写进 XML? | SQL 长、要连表、还要动态变化——55 篇的规则:复杂 SQL 用 XML;同包同名、namespace 是接口全限定名、id 是方法名 |
| 六个参数怎么接收更好? | 封装成 EmpQueryParam 对象(page/pageSize 默认值写在字段上、@DateTimeFormat 也挪到字段上);参数少用形参、参数多用对象 |
| 条件写死有什么问题? | 用户没填的条件是 null,而 字段 = null 永远不成立 → 一行都查不出来;条件组合太多也不可能穷举 |
| 什么是动态 SQL? | 随着用户的输入或外部条件的变化而变化的 SQL 语句 |
<if> 和 <where> 各干什么? | <if> 条件成立才拼接 SQL 片段;<where> 生成 where 关键字并去掉多余的 and/or(没有条件时连 where 都不生成) |
| 本机实测怎么证明动态 SQL 生效了? | 无条件时日志里的 SQL 连 where 都没有;带姓名 + 性别时只拼了这两个条件,日期条件整段消失 |
| 分页和条件会互相影响吗? | 不会——条件由 <if>/<where> 拼进原 SQL,分页由 PageHelper 自动加 LIMIT,日志里能同时看到两者 |
| 中文条件查不到数据是什么原因? | Windows 命令行下 curl 把中文按 GBK 发出去,服务端收到 ??(本机实测 total 为 0);改用 URL 编码(?name=%E6%9D%8E → total 6 条)或 Apifox/浏览器 |
name(姓名)、gender(性别)、begin/end(入职日期范围),接口文档里四个都是”否”(可以不填)PageResult;Mapper 的 SQL 里带上条件LocalDate 类型 + @DateTimeFormat(pattern = "yyyy-MM-dd")——不指定格式,2007-09-01 这样的字符串转不成日期;格式要和前端传的一致@RequestParam 用于参数名不一致时指定名字、以及用 defaultValue 设置默认值;@DateTimeFormat 用于接收日期时间类型参数并指定日期格式namespace 是接口全限定名、id 是方法名、resultType 是单条记录类型)EmpQueryParam(@Data);默认值写在字段上(page = 1、pageSize = 10);@DateTimeFormat 也挪到字段上;queryString 参数少用形参、多用对象null,字段 = null 永远不成立 → 查不出数据;条件组合太多,不可能每种都写一个方法<if test="…"> 条件成立才拼接 SQL 片段;<where> 生成 where 关键字并去掉多余的 and/or;无条件时连 where 都不生成(本机实测日志可证)total 是 20;不带条件时 total 是 30??、total 0;URL 编码后(?name=%E6%9D%8E)正常返回 total 6 条 2-1 让控制层能接收”分页 + 条件”六个参数
需求:员工列表接口要接收这些东西——页码、每页记录数(不传时默认第 1 页、每页 10 条),姓名(可以不填)、性别(可以不填)、入职日期的开始和结束(可以不填,前端传的是 2007-09-01 这样的字符串)。要求这些参数能正确接收,日期参数不能报类型转换错误。
(练习文件 test_68_条件分页与动态SQL.java 的题目2-1 里给了写作区。)
一级 · 思路:六个形参排一排——两个分页参数要默认值,两个简单条件直接收,两个日期要告诉 Spring 按什么格式解析
二级 · 方法:分页参数用 @RequestParam(defaultValue = …);姓名用 String、性别用 Integer;日期用 LocalDate + @DateTimeFormat(pattern = "yyyy-MM-dd");方法里先用 log.info 打印参数,再调 Service
三级 · 骨架:@GetMapping public Result page(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer pageSize, String name, Integer gender, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin, @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) { log.info(…); PageResult<Emp> pageResult = empService.page(…); return Result.success(pageResult); }
1@GetMapping2public Result page(@RequestParam(defaultValue = "1") Integer page,3 @RequestParam(defaultValue = "10") Integer pageSize,4 String name, Integer gender,5 @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,6 @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){7 log.info("分页查询,参数:{},{},{},{},{},{}", page, pageSize, name, gender, begin, end);8 PageResult<Emp> pageResult = empService.page(page, pageSize, name, gender, begin, end);9 return Result.success(pageResult);10}说明:name/gender 不加注解也能收(形参名和 queryString 的 key 一致);两个日期参数必须有 @DateTimeFormat,否则 2007-09-01 转不成 LocalDate,请求直接失败。日志那一行六个 {} 要按顺序对上实参——联调时先看它,能立刻确认”条件到底传进来没有”。
2-2 把六个请求参数封装成一个对象接收
需求:控制层方法的参数已经有六个,以后还可能加(职位、薪资范围……),方法签名越来越难维护。请把这一批请求参数封装成一个对象来接收,要求:页码和每页记录数不传时仍有默认值(1 和 10)、日期参数照样能按 2007-09-01 的格式解析;控制层方法体里的日志和 Service 调用也要跟着改。
(练习文件 test_68_条件分页与动态SQL.java 的题目2-2 里给了写作区。)
一级 · 思路:建一个 POJO 装这六个参数,属性名和 queryString 的 key 一模一样;默认值从”注解”搬到”字段初始值”
二级 · 方法:类名 EmpQueryParam,标 @Data;page = 1、pageSize = 10 直接写字段初始值;@DateTimeFormat(pattern = "yyyy-MM-dd") 标在 begin/end 字段上;控制层改成 page(EmpQueryParam empQueryParam)
三级 · 骨架:@Data public class EmpQueryParam { private Integer page = 1; private Integer pageSize = 10; private String name; private Integer gender; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate begin; @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate end; } + @GetMapping public Result page(EmpQueryParam empQueryParam){ log.info("分页查询,参数:{}", empQueryParam); PageResult<Emp> pageResult = empService.page(empQueryParam); return Result.success(pageResult); }
1@Data2public class EmpQueryParam {3 private Integer page = 1; // 当前页码4 private Integer pageSize = 10; // 每页记录数5 private String name; // 员工姓名6 private Integer gender; // 员工性别7 @DateTimeFormat(pattern = "yyyy-MM-dd")8 private LocalDate begin; // 入职日期9 @DateTimeFormat(pattern = "yyyy-MM-dd")10 private LocalDate end; // 入职日期11}1@GetMapping2public Result page(EmpQueryParam empQueryParam){3 log.info("分页查询,参数:{}", empQueryParam);4 PageResult<Emp> pageResult = empService.page(empQueryParam);5 return Result.success(pageResult);6}说明:Spring 能自动把 URL 上的参数装进这个对象,靠的是属性名与参数名一致(?name=张 → name 属性)。两处容易忘:① 默认值要写在字段上(对象接收没有 defaultValue 可用);② @DateTimeFormat 也要跟着挪到字段上。以后加条件只在类里加字段,控制层方法一个字都不用改——这就是封装的收益。
2-3 在 XML 里写一条”条件写死”的多表条件查询 SQL
需求:员工列表要按姓名(模糊匹配)、性别(精确匹配)、入职日期范围(含头含尾)查询,同时带出部门名称,结果按最后修改时间倒序。请把这条 SQL 写进 XML 映射文件里,并说明文件该放在哪、namespace 和 id 该写什么。
(练习文件 test_68_动态SQL配置.xml 的题目2-3 里给了写作区。)
一级 · 思路:先想 SQL 本身(连表 + 三个条件 + 排序),再套 XML 的外壳(55 篇的三条规则)
二级 · 方法:文件放 src/main/resources/com/itheima/mapper/EmpMapper.xml(与接口同包同名);<mapper namespace="com.itheima.mapper.EmpMapper">;<select id="list" resultType="com.itheima.pojo.Emp">;模糊匹配用 like concat('%',#{name},'%'),范围用 between #{begin} and #{end},别名用 d.name deptName
三级 · 骨架:<?xml version="1.0" encoding="UTF-8" ?> + <!DOCTYPE mapper …> + <mapper namespace="____"> <select id="____" resultType="____"> select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id where ____ </select> </mapper>
1<?xml version="1.0" encoding="UTF-8" ?>2<!DOCTYPE mapper3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"4 "https://mybatis.org/dtd/mybatis-3-mapper.dtd">5<mapper namespace="com.itheima.mapper.EmpMapper">6
7 <select id="list" resultType="com.itheima.pojo.Emp">8 select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id9 where e.name like concat('%',#{name},'%') and e.gender = #{gender} and e.entry_date between #{begin} and #{end}10 order by e.update_time desc11 </select>12
13</mapper>说明:文件放 src/main/resources/com/itheima/mapper/EmpMapper.xml(与 EmpMapper 接口”同包同名”);namespace 是接口全限定名 com.itheima.mapper.EmpMapper;语句 id="list" 与接口方法名一致;resultType 写单条记录的类型(接口返回 List<Emp>,这里还是写 com.itheima.pojo.Emp)。这条 SQL 跑起来”看起来”没问题——只要三个条件都填。下一题就来看它的毛病。
2-4 把上面那条 SQL 改成动态 SQL
需求:上面那条 SQL 的条件是写死的——用户只填姓名、不填性别和日期时,一行数据都查不出来。请把它改成”用户填了哪个条件就按哪个条件查,一个条件都不填就查全部”,并说明两个标签各自解决了什么问题。
(练习文件 test_68_动态SQL配置.xml 的题目2-4 里给了写作区。)
一级 · 思路:每个条件都用”有没有值”包起来;where 关键字和多余的 and 交给标签去管
二级 · 方法:每个条件外面套 <if test="…">(字符串要同时判 != null 和 != '';日期范围两个参数都有值才拼);外面套 <where>;order by 放在 <where> 外面
三级 · 骨架:<where> <if test="name != null and name != ''"> e.name like concat('%',#{name},'%') </if> <if test="gender != null"> and e.gender = #{gender} </if> <if test="begin != null and end != null"> and e.entry_date between #{begin} and #{end} </if> </where> order by e.update_time desc
1<select id="list" resultType="com.itheima.pojo.Emp">2 select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id3 <where>4 <if test="name != null and name != ''">5 e.name like concat('%',#{name},'%')6 </if>7 <if test="gender != null">8 and e.gender = #{gender}9 </if>10 <if test="begin != null and end != null">11 and e.entry_date between #{begin} and #{end}12 </if>13 </where>14 order by e.update_time desc15</select>两个标签解决的问题:<if> 让”填了的条件才拼进 SQL”——没填的条件整段消失(不是拼成 = null);<where> 负责生成 where 关键字、并去掉第一个条件前多余的 and(所以后两个条件都写 and、第一个不写也能拼对),一个条件都没有时连 where 都不生成。本机实测的日志正是这样:带姓名 + 性别时 WHERE e.name LIKE concat('%', ?, '%') AND e.gender = ?,不带条件时 … d.id order by …(连 where 都没有),两种情况都能正常返回数据。
3-1 给员工列表加上条件分页查询,并用日志和实测把三个问题回答清楚 这一题把本节从头到尾做一遍:先按”条件写死”跑通,再改成动态 SQL,最后用实测数据回答”动态 SQL 到底做了什么”。
yyyy-MM-dd 解析);namespace 与 id 按 55 篇的规则),先写条件写死的版本;/emps?name=李&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=5),记下 total 和 rows 的条数;/emps?begin=2007-09-01&end=2022-09-01&page=1&pageSize=5),观察返回的 total——为什么它和刚才不一样? 如果只填姓名、其余空着,会发生什么?/emps?page=1&pageSize=10),把服务端日志里那条 SQL 抄下来——注意里面有没有 where;name=李),记录 total;再回答:为什么命令行 curl 传中文会查不到数据、该怎么处理?<if> 和 <where> 各解决了什么问题?涉及知识点
| 知识点 | 在这里的应用 |
|---|---|
| 参数接收(queryString) | 第 1 步——简单参数直接收、日期用 @DateTimeFormat 指定格式 |
| 请求参数封装 | 第 1、3 步——EmpQueryParam 装六个参数,默认值写在字段上 |
| XML 映射配置 | 第 2 步——同包同名、namespace、id、resultType |
| PageHelper 分页 | 第 3、5、8 步——startPage + 解析 Page,日志里看 LIMIT |
动态 SQL(<if>/<where>) | 第 6、7、8 步——条件按需拼接、无条件时不生成 where |
| 联调与编码 | 第 7 步——中文参数的 URL 编码 |
一级 · 思路:先”能查出数据”(条件都填),再故意”少填条件”制造问题,然后用动态 SQL 修掉它——每一步都用日志和 total 做证据
二级 · 方法:@DateTimeFormat(pattern = "yyyy-MM-dd");XML 里 like concat('%',#{name},'%') / between #{begin} and #{end};动态 SQL 用 <where> 包 <if test="…">;请求用 Apifox 或对中文做 URL 编码
三级 · 骨架:控制层 page(EmpQueryParam empQueryParam) → 业务层 PageHelper.startPage(…) + empMapper.list(empQueryParam) → XML 的 <where> + 三个 <if>;对比观察点:日志里 WHERE 后拼了哪些条件、LIMIT 的参数是几、total 是 20 还是 30
1~3. 三个文件:控制层与 EmpQueryParam 见 2-1、2-2 的参考答案;XML 见 2-4 的参考答案;业务层:
1@Override2public PageResult<Emp> page(EmpQueryParam empQueryParam) {3 //1. 设置分页参数(PageHelper)4 PageHelper.startPage(empQueryParam.getPage(), empQueryParam.getPageSize());5 //2. 执行查询6 List<Emp> empList = empMapper.list(empQueryParam);7 //3. 解析查询结果,并封装8 Page<Emp> p = (Page<Emp>) empList;9 return new PageResult<Emp>(p.getTotal(), p.getResult());10}4. 三个条件都填(本机实测):/emps?name=李&gender=1&begin=2007-09-01&end=2022-09-01&page=1&pageSize=5 能查出数据,日志里 WHERE 后面是 e.name like concat('%', ?, '%') and e.gender = ? and e.entry_date between ? and ?——三个条件都在。
5. 只填日期范围(本机实测):/emps?begin=2007-09-01&end=2022-09-01&page=1&pageSize=5 返回 total 为 20、rows 5 条;它和不带条件时(total 为 30)不一样,因为多了一个日期范围限制,符合条件的员工变少了。而如果条件写死、只填姓名、性别和日期空着,SQL 里的 e.gender = null 和 e.entry_date between null and null 永远不成立,结果是一行都查不出来——这就是必须改成动态 SQL 的原因。
6. 改成动态 SQL 后的无条件请求(本机实测):
1==> Preparing: SELECT count(0) FROM emp e LEFT JOIN dept d ON e.dept_id = d.id2==> Preparing: select e.*, d.name deptName from emp e left join dept d on e.dept_id = d.id3 order by e.update_time desc LIMIT ?, ?SQL 里没有 where(… d.id order by … 直接接排序)——<where> 标签发现一个 <if> 都没成立,就没生成 where 关键字;这正是动态 SQL 生效的证据。
7. 中文条件:用 Apifox 发 name=李 能正常返回(本机实测:把中文做 URL 编码后 ?name=%E6%9D%8E 返回 total 6 条);而 Windows 命令行里 curl --get --data-urlencode "name=张" 会把中文按 GBK 发出去,服务端日志里参数变成 ??、total 为 0。处理办法:先把中文做 URL 编码再传,或者干脆用 Apifox / 浏览器 / 前端页面发请求(它们会自动编码)。
8. 三个问题的答案:
null,而 SQL 里 字段 = null(或 between null and null)永远不成立,所以整个 where 条件都为假,一行都查不出来。<if> 解决”条件个数不固定”:填了的条件才拼进 SQL,没填的整段消失;<where> 解决”where 关键字和 and 的摆放”:自动生成 where、去掉多余的 and/or,一个条件都没有时连 where 都不生成。where 那一段,PageHelper 管的是在 SQL 末尾加 LIMIT,两者互不干扰——日志里 … order by e.update_time desc LIMIT ?, ?(第 2 页时参数是 5, 5)就是证据;返回的 total 也仍然是”符合当前条件的总记录数”。如果你喜欢,那么欢迎来到我的世界!
了解更多暂未播放



"所有的相遇都是久别重逢。"—— 未知


