1. 项目概述
"苍穹外卖-day03-新增菜品模块"是一个典型的外卖系统后台管理功能开发案例,主要聚焦于菜品管理模块的数据持久层实现。作为外卖系统核心业务模块之一,菜品管理需要处理复杂的分类关联、多条件查询和批量操作,这正是MyBatis框架大显身手的场景。
我在实际开发中发现,很多新手在实现这类餐饮系统时,最容易在三个地方栽跟头:一是菜品与分类的关联关系处理,二是动态SQL的合理运用,三是批量操作的性能优化。本文将结合MyBatis的XML映射文件编写,手把手带你避开这些坑。
2. 核心需求解析
2.1 业务场景分析
外卖系统的菜品管理模块通常需要支持以下核心功能:
- 多级分类体系(如川菜/湘菜等大类下的细分)
- 菜品基础信息管理(名称、价格、描述等)
- 复杂查询条件组合(分类+状态+关键词等)
- 批量上下架操作
- 销量统计与排序
2.2 技术选型考量
为什么选择MyBatis而不是JPA?在餐饮系统中:
- 需要精细控制SQL性能(特别是分页查询)
- 存在大量动态查询条件
- 需要处理复杂的统计报表SQL
- 有批量操作需求(如节日菜品批量上架)
提示:虽然JPA在简单CRUD上更便捷,但外卖系统这类需要高度定制SQL的场景,MyBatis的灵活性优势明显
3. MyBatis XML映射实战
3.1 基础映射配置
先看一个完整的菜品映射文件骨架:
xml复制<!-- DishMapper.xml -->
<mapper namespace="com.cangqiong.mapper.DishMapper">
<resultMap id="BaseResultMap" type="com.cangqiong.entity.Dish">
<id column="id" property="id" jdbcType="BIGINT"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
<result column="category_id" property="categoryId" jdbcType="BIGINT"/>
<!-- 其他字段映射 -->
</resultMap>
</mapper>
3.2 动态SQL实现
菜品列表查询的典型动态SQL示例:
xml复制<select id="selectByCondition" resultMap="BaseResultMap">
SELECT * FROM dish
<where>
<if test="name != null and name != ''">
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 update_time DESC
</select>
3.3 关联查询技巧
处理菜品与分类的关联查询时,推荐两种方案:
方案一:JOIN查询(适合简单关联)
xml复制<resultMap id="WithCategoryMap" type="com.cangqiong.entity.Dish" extends="BaseResultMap">
<association property="category" javaType="com.cangqiong.entity.Category">
<id column="c_id" property="id"/>
<result column="c_name" property="name"/>
</association>
</resultMap>
方案二:二次查询(适合复杂关联)
xml复制<resultMap id="WithCategoryMap" type="com.cangqiong.entity.Dish" extends="BaseResultMap">
<association property="category" column="category_id"
select="com.cangqiong.mapper.CategoryMapper.selectById"/>
</resultMap>
4. 批量操作优化
4.1 批量插入实现
节日菜品批量上架的优化写法:
xml复制<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO dish(name, price, category_id, ...)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.name}, #{item.price}, #{item.categoryId}, ...)
</foreach>
</insert>
4.2 批量状态更新
应对双11活动批量修改菜品状态:
xml复制<update id="batchUpdateStatus">
UPDATE dish SET status = #{status}
WHERE id IN
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</update>
5. 踩坑实录与解决方案
5.1 分页查询性能优化
错误做法:
xml复制<!-- 这种写法在大数据量时性能极差 -->
<select id="selectPage" resultMap="BaseResultMap">
SELECT * FROM dish LIMIT #{pageNum}, #{pageSize}
</select>
正确方案:
xml复制<select id="selectPage" resultMap="BaseResultMap">
SELECT * FROM dish
WHERE id >= (
SELECT id FROM dish
ORDER BY id
LIMIT #{pageNum}, 1
)
LIMIT #{pageSize}
</select>
5.2 动态SQL中的类型判断
处理前端可能传入数组或字符串的场景:
xml复制<select id="selectByIds" resultMap="BaseResultMap">
SELECT * FROM dish
WHERE id IN
<choose>
<when test="ids instanceof java.util.List">
<foreach collection="ids" item="id" open="(" separator="," close=")">
#{id}
</foreach>
</when>
<otherwise>
(#{ids})
</otherwise>
</choose>
</select>
5.3 XML中的特殊字符处理
菜品描述可能包含HTML内容时的处理:
xml复制<update id="updateDescription">
UPDATE dish SET description = #{description, jdbcType=VARCHAR}
WHERE id = #{id}
</update>
在Java代码中:
java复制// 使用CDATA包装特殊内容
String desc = "<![CDATA[" + htmlContent + "]]>";
dishMapper.updateDescription(dishId, desc);
6. 扩展思考
6.1 MyBatis-Plus的适用场景
虽然原生MyBatis更灵活,但在简单CRUD场景下,MyBatis-Plus能极大提升效率:
java复制// 例如实现分页查询只需:
Page<Dish> page = new Page<>(pageNum, pageSize);
LambdaQueryWrapper<Dish> wrapper = Wrappers.lambdaQuery();
wrapper.eq(Dish::getStatus, 1);
dishMapper.selectPage(page, wrapper);
6.2 复杂统计报表处理
对于需要多表关联的销售统计,推荐使用:
- 存储过程 + MyBatis调用
- 专用SQL构建工具如QueryDSL
- 自定义ResultHandler处理复杂结果集
6.3 缓存策略选择
菜品这类读多写少的数据,建议:
- 一级缓存:默认开启(SqlSession级别)
- 二级缓存:通过
标签配置(Mapper级别) - 分布式缓存:整合Redis实现
我在实际项目中发现,对于价格等敏感数据,建议设置flushInterval="60000"(1分钟自动刷新),既保证性能又避免脏读。
