【苍穹外卖Day4】两大接口详解和其他拓展
本文重点回顾了新增套餐和分页查询功能实现。新增套餐涉及4个接口,核心是通过事务管理确保套餐表和关系表的原子性操作,利用MyBatis主键回显获取ID。分页查询使用PageHelper插件实现,通过startPage()方法自动拦截SQL添加LIMIT子句,返回Page对象包含分页信息。文中详细解析了Lombok的@Builder注解、MyBatis动态SQL、事务注解作用及PageHelper工作
- 新增套餐
- 套餐分页查询
- 删除套餐
- 修改套餐
- 起售停售套餐
这里我只重点回顾新增和分页查询。因为新增菜品我卡了比较久,分页查询我对那个插件有点不熟悉,所以重点回顾这两个。
新增套餐
接口设计(共涉及到4个接口):
- 根据类型查询分类(已完成)用于页面展示套餐有哪些类型
- 根据分类id查询菜品
- 图片上传(已完成)//这是一个通用方法
- 新增套餐
查询菜品,返回菜品集合
从上到下依次是controller,serviceimpl和dao
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<Dish>> list(Long categoryId){
List<Dish> list = dishService.list(categoryId);
return Result.success(list);
}
在类上加上
@Builder后,
Lombok 会自动帮你生成一个“构建器”,
让你可以用 链式调用 的方式创建对象,而不是写一大堆构造函数。
/**
* 根据分类id查询菜品
* @param categoryId
* @return
*/
public List<Dish> list(Long categoryId) {
Dish dish = Dish.builder()
.categoryId(categoryId)
.status(StatusConstant.ENABLE)
.build();
return dishMapper.list(dish);
}
字段名解析
| 属性 | 含义 | 对应内容 |
|---|---|---|
<select> |
查询语句标签 | 对应 Mapper 中的方法 |
id |
SQL 唯一标识 | 对应方法名 |
parameterType |
入参类型 | 方法参数类型 |
resultType |
返回对象类型 | 查询结果映射类 |
注意:
MyBatis 的 resultType 指定的是 “每一行结果” 的类型,而不是整个返回集合的类型。
就是说加入查询到100条dish数据,每一行返回的类型都是dish
<select id="list" resultType="Dish" parameterType="Dish">
select * from dish
<where>
<if test="name != null">
and name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
order by create_time desc
</select>
前端展示
新增套餐接口实现
/**
* 套餐管理
*/
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {
@Autowired
private SetmealService setmealService;
/**
* 新增套餐
* @param setmealDTO
* @return
*/
@PostMapping
@ApiOperation("新增套餐")
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
}
/**
* 套餐业务实现
*/
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {
@Autowired
private SetmealMapper setmealMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
@Autowired
private DishMapper dishMapper;
/**
* 新增套餐,同时需要保存套餐和菜品的关联关系
* @param setmealDTO
*/
@Transactional
@Override
public void saveWithDish(SetmealDTO setmealDTO) {
//请求参数用的是SetmealDTO类封装的,包含套餐数据以及套餐菜品关系表数据,
// 这个地方只需要插入套餐的基本信息,所以进行属性拷贝。
Setmeal setmeal = new Setmeal();
BeanUtils.copyProperties(setmealDTO, setmeal);
//向套餐表插入数据
setmealMapper.insert(setmeal);
//获取生成的套餐id 通过sql中的useGeneratedKeys="true" keyProperty="id"获取插入后生成的主键值
//套餐菜品关系表的setmealId页面不能传递,它是向套餐表插入数据之后生成的主键值,也就是套餐菜品关系表的逻辑外键setmealId
Long setmealId = setmeal.getId();
//获取页面传来的套餐和菜品关系表数据
List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
//遍历关系表数据,为关系表中的每一条数据(每一个对象)的setmealId赋值,
// 这个地方不需要像之前写新增菜品时多写个if判断,因为之前的口味数据是非必须的,
// 这个地方要求套餐必须包含菜品是必须的,所以不需要if判断,不存在套餐不包含菜品得情况
setmealDishes.forEach(setmealDish -> {
//将Setmeal套餐类的id属性赋值给SetmealDish套餐关系类的setmealId
//套餐表的id保存在套餐关系表充当外键为setmealId
setmealDish.setSetmealId(setmealId);
});
//保存套餐和菜品的关联关系 动态sql批量插入
setmealDishMapper.insertBatch(setmealDishes);
}
}
-
主键自增机制
在数据库(例如 MySQL)中,setmeal表的主键通常是自增的(AUTO_INCREMENT)。
当我们执行setmealMapper.insert(setmeal)时,数据库会自动为这条套餐数据生成一个新的主键 ID。 -
主键回显(useGeneratedKeys)
在 MyBatis 的映射文件中,配置了:<insert id="insert" useGeneratedKeys="true" keyProperty="id">这表示插入完成后,数据库生成的主键会被自动封装回 Java 对象中,注意是返回到对象中。
于是,setmeal.getId()就能直接获取刚刚插入套餐的主键值。
为什么要执行多条 SQL?
因为这里涉及两张表的操作:
-
插入套餐表(主表)
setmealMapper.insert(setmeal);向
setmeal表写入套餐的基本信息。 -
插入套餐菜品关系表(子表)
setmealDishMapper.insertBatch(setmealDishes);-
每个
SetmealDish对象对应一条套餐-菜品关系数据。 -
因为一个套餐对应多个菜品,所以需要批量插入,这里会用到动态sql。
-
这两步 SQL 不能合并执行,因为要先拿到主表的 ID。
-
因此,这种操作通常会产生 两次数据库写操作(多条 SQL)。
@Transactional 的作用
@Transactional(事务注解)保证了这两次数据库操作的原子性:
如果第一步(插入套餐)成功,但第二步(插入关系表)失败,事务会自动回滚,第一步的插入也会被撤销。
这样可以防止出现“主表有数据但子表没有”的数据不一致问题。
只有当所有 SQL 都执行成功后,Spring 才会提交事务。
原子性(Atomicity)
事务是一个不可分割的操作单元,要么全部成功,要么全部失败。
一致性(Consistency)
事务执行前后,数据必须保持一致性。
xml语句省略
套餐分页查询
- 根据页码进行分页展示
- 每页展示10条数据
- 可以根据需要,按照套餐名称、分类、售卖状态进行查询
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分页查询")
public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
return Result.success(pageResult);
}
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
int pageNum = setmealPageQueryDTO.getPage();
int pageSize = setmealPageQueryDTO.getPageSize();
//需要在查询功能之前开启分页功能:当前页的页码 每页显示的条数
PageHelper.startPage(pageNum, pageSize);
//这个方法有返回值为Page对象,里面保存的是分页之后的相关数据
Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
//封装到PageResult中:总记录数 当前页数据集合
return new PageResult(page.getTotal(), page.getResult());
}
动态sql,用来实现多种条件查询。左外连接实现查询套餐的同时查询套餐的种类。
<select id="pageQuery" resultType="com.sky.vo.SetmealVO">
select
s.*,c.name categoryName
from
setmeal s
left join
category c
on
s.category_id = c.id
<where>
<if test="name != null">
and s.name like concat('%',#{name},'%')
</if>
<if test="status != null">
and s.status = #{status}
</if>
<if test="categoryId != null">
and s.category_id = #{categoryId}
</if>
</where>
order by s.create_time desc
</select>
PageHelper插件解析
PageHelper.startPage(pageNum, pageSize)
这是 PageHelper 提供的 静态方法,用于在执行查询前开启分页功能。
✅ 作用:
告诉 PageHelper:
“我接下来要执行一个查询,这个查询只需要第
pageNum页的数据,每页pageSize条。”
⚙️ 工作原理:
PageHelper 使用 线程局部变量(ThreadLocal) 记录分页参数。
当接下来 MyBatis 执行 select 查询时,PageHelper 会自动:
-
拦截 SQL;
-
在 SQL 语句后追加
LIMIT子句;LIMIT (pageNum - 1) * pageSize, pageSize比如:
pageNum = 2, pageSize = 5
那 SQL 会变成:SELECT * FROM setmeal LIMIT 5, 5; -
同时执行一条
COUNT(*)语句,用于计算总记录数
Page 是 PageHelper 提供的一个 分页结果类,继承自 ArrayList。
✅ Page 对象的主要属性:
| 属性名 | 类型 | 作用 |
|---|---|---|
pageNum |
int | 当前页码 |
pageSize |
int | 每页记录数 |
total |
long | 总记录数(通过 COUNT 统计得出) |
pages |
int | 总页数(由 total / pageSize 计算得出) |
result |
List<T> | 当前页的结果集(即你查到的这页数据) |
startRow |
long | 当前页第一条记录在数据库中的行号 |
endRow |
long | 当前页最后一条记录的行号 |
PageResult 是一个自定义封装类,一般包含:
private long total; // 总记录数
private List<?> records; // 当前页的数据
前端拿到后,就能渲染出分页列表(比如总页数、当前页数据等)。
PageHelper.startPage()负责告诉分页插件“我要第几页,每页几条”;Page<T>接收分页后的结果数据和分页信息;
最终通过PageResult封装返回,形成完整的分页查询逻辑。
前端展示:

总结
这一天的练习让我对多表查询,sql语句,分页查询插件(底层是拦截器),事务等等有了进一步的认识。对基础crud帮助挺大的,至少简单接口随便写,复杂接口要耗费一点精力写出来。
更多推荐
所有评论(0)