• 新增套餐
  • 套餐分页查询
  • 删除套餐
  • 修改套餐
  • 起售停售套餐

这里我只重点回顾新增和分页查询。因为新增菜品我卡了比较久,分页查询我对那个插件有点不熟悉,所以重点回顾这两个。

新增套餐

接口设计(共涉及到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?

因为这里涉及两张表的操作:

  1. 插入套餐表(主表)

    setmealMapper.insert(setmeal);

    setmeal 表写入套餐的基本信息。

  2. 插入套餐菜品关系表(子表)

    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帮助挺大的,至少简单接口随便写,复杂接口要耗费一点精力写出来。

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐