1. Controller 层
1.1 示例代码(含详细备注)
@GetMapping("/admin/dish/page")
public Result<PageResult> page(DishPageQueryDTO dto) {
/*
* 说明:
* 1. Spring MVC 会自动将 Query 参数绑定到 dto 中
* 例如:/page?page=1&pageSize=10&name=鱼
* dto.page = 1
* dto.pageSize = 10
* dto.name = "鱼"
*
* 2. Controller 不处理业务,仅调用 service 层
*/
PageResult result = dishService.page(dto);
// 使用统一响应结构
return Result.success(result);
}
1.2 关键细节解释
-
参数封装方式
-
Spring 自动将 URL 查询参数映射到 DTO 中(按字段名匹配)。
-
同名参数会被完整填充,无需使用 @RequestParam。
-
-
Controller 不执行分页逻辑
- 分页逻辑完全放在 service 层,使 Controller 保持轻量与无业务状态。
-
返回类型使用 Result
-
Result 为统一响应包装类。
-
PageResult 存储 total 和 records(分页数据)。
-
-
DTO 字段规则
DTO 的字段名必须与前端传参保持一致,如 page、pageSize、name 等,Spring 才能自动封装。
1.3 问题总结
-
Query 参数能否自动封装为对象?
可以,只需参数名与 DTO 字段名一致。
-
是否必须用 @RequestParam?
不是。分页查询使用 DTO 封装更规范。
-
Controller 能否直接调用 PageHelper?
不建议。分页逻辑属于业务层,由 service 层统一处理。
2. ServiceImpl 层
2.1 示例代码(含详细备注)
@Override
public PageResult page(DishPageQueryDTO dto) {
/*
* PageHelper.startPage 必须放在查询前执行。
* 内部原理:
* 1. 创建 Page 对象并绑定到 ThreadLocal
* 2. 拦截接下来执行的 SQL
* 3. 自动拼接 limit offset
* 4. 自动执行 count 查询获取总数
*/
PageHelper.startPage(dto.getPage(), dto.getPageSize());
// 执行 Mapper 查询。此时 SQL 会被分页插件自动改写。
List<DishVO> list = dishMapper.page(dto);
/*
* PageInfo 的作用:
* 1. 识别 list 是否为 Page 类型
* 2. 自动读取 total 和当前页数据
*/
PageInfo<DishVO> pageInfo = new PageInfo<>(list);
// 封装为通用分页返回结构
return new PageResult(pageInfo.getTotal(), pageInfo.getList());
}
2.2 关键细节解释
-
PageHelper.startPage 的作用
创建分页上下文,使下一条 SQL 自动拼接分页语句。
offset = (page - 1) * pageSize
-
Page 对象与 PageInfo 的关联
-
list 在 PageHelper 处理后会成为 Page 类型
-
PageInfo 可以从 list 中读取分页信息(total, records)
-
-
PageHelper 必须在执行 SQL 之前调用
如果写在 SQL 之后,则不会生效。
-
返回统一的 PageResult
PageResult 包含:
-
total:总条数
-
records:当前页数据列表
-
2.3 常见问题总结
-
PageHelper.startPage 是否会执行 SQL?
不执行,只是设置上下文。
-
PageHelper 如何知道要分页哪条 SQL?
startPage 与下一条 mapper 查询绑定,通过拦截器实现。
-
PageInfo 必须 new 吗?
是的,用于自动解析分页数据。
-
ServiceImpl 是否需要 try-catch?
可选,按项目统一规范处理。
3. Mapper XML 层
3.1 示例 SQL(含详细备注)
<select id="page" resultType="com.sky.vo.DishVO">
/*
* 说明:
* 1. 通过 LEFT JOIN 关联 category 表,获取分类名称
* 2. 使用别名将不一致字段映射到 VO 字段
* 3. 动态构建 WHERE 条件
*/
SELECT
d.id,
d.name,
d.price,
d.status,
-- 数据库字段 update_time 与 VO 字段 updateTime 不同,必须指定别名
d.update_time AS updateTime,
-- 分类名称来自 category 表
c.name AS categoryName
FROM dish d
LEFT JOIN category c ON d.category_id = c.id
<where>
<!-- 模糊搜索 name -->
<if test="name != null and name != ''">
AND d.name LIKE CONCAT('%', #{name}, '%')
</if>
<!-- 分类过滤 -->
<if test="categoryId != null">
AND d.category_id = #{categoryId}
</if>
<!-- 状态过滤 -->
<if test="status != null">
AND d.status = #{status}
</if>
</where>
ORDER BY d.update_time DESC
</select>
3.2 关键细节解释
-
JOIN 逻辑说明
菜品表不包含 categoryName,因此需要关联 category 表的 name 字段。
-
字段别名规则
-
当数据库字段与 VO 字段名不同,必须使用 AS
-
MyBatis 使用字段名匹配映射(非位置匹配)
-
-
标签处理逻辑
-
自动添加 WHERE
-
自动剔除多余 AND/OR
-
若所有 条件均不成立,则不会生成 WHERE
-
-
动态 SQL
根据 DTO 字段是否为 null 或空字符串决定是否拼接 SQL 条件。
3.3 常见问题总结
-
为什么能自动去掉第一个 AND?
因为 MyBatis 会对生成的 SQL 进行修剪处理。
-
字段名不一致为什么需要 AS?
MyBatis 使用字段名与 VO 字段名直接匹配,不一致不会自动映射。
-
查询时 DTO 字段为 null 会怎样?
不会拼接对应 SQL 片段。
-
是否必须使用 LEFT JOIN?
若希望无分类的菜品也能展示,则使用 LEFT JOIN;否则 INNER JOIN 会过滤掉无分类的数据。