
1. 苍穹外卖项目背景与核心需求苍穹外卖是一个典型的餐饮行业SaaS系统采用Java技术栈构建。在day03的课程中我们重点处理的是菜品与套餐这两个核心业务模块的删除功能实现。作为订单系统的基础数据菜品和套餐的删除并非简单的数据库记录移除而是需要考虑复杂的业务关联和一致性约束。在实际业务场景中一个菜品可能被多个套餐包含而套餐又可能被大量历史订单引用。如果允许随意删除会导致已售出的套餐在订单详情中显示该商品已下架统计报表中的销售数据失去关联商品信息商家无法追溯历史订单的完整商品信息因此我们需要在删除操作前进行双重校验关联套餐校验确保菜品没有被任何套餐引用销售状态校验确保菜品/套餐没有未完成的订单关联2. 删除功能的技术架构设计2.1 整体技术栈选型项目采用经典的Java EE分层架构持久层MyBatis 自定义Mapper业务层Spring Service控制层Spring MVC数据库MySQL 8.0删除操作的技术实现路径Controller → Service → Mapper → DB2.2 关键SQL设计原则在MyBatis中处理删除操作时我们需要特别注意!-- 不推荐的写法 -- delete iddeleteDish parameterTypelong DELETE FROM dish WHERE id #{id} /delete !-- 推荐的写法 -- delete iddeleteDish parameterTypelong UPDATE dish SET status 0, update_time NOW() WHERE id #{id} AND status 1 /delete采用逻辑删除而非物理删除的原因保留数据完整性便于审计追溯避免级联删除导致的意外数据丢失符合餐饮行业下架而非销毁的业务语义3. 双重校验机制的实现细节3.1 关联套餐校验实现在删除菜品前需要检查该菜品是否被任何套餐引用Transactional public void deleteDish(Long dishId) { // 校验1是否被套餐引用 Integer count dishMapper.countReferencedBySetmeal(dishId); if (count 0) { throw new BusinessException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL); } // 执行逻辑删除 dishMapper.logicDelete(dishId); }对应的Mapper SQLselect idcountReferencedBySetmeal resultTypeint SELECT COUNT(1) FROM setmeal_dish WHERE dish_id #{dishId} AND is_deleted 0 /select3.2 销售状态校验实现对于套餐删除还需要额外检查是否有未完成订单public void deleteSetmeal(Long setmealId) { // 校验1是否有未完成订单 Integer count orderMapper.countUnfinishedBySetmeal(setmealId); if (count 0) { throw new BusinessException(MessageConstant.SETMEAL_ON_ORDER); } // 执行逻辑删除 setmealMapper.logicDelete(setmealId); }订单检查SQL应该考虑多种状态SELECT COUNT(1) FROM orders WHERE setmeal_id #{setmealId} AND status IN (1, 2, 3) -- 1待付款 2待接单 3已接单4. 防御性编程与异常处理4.1 并发操作处理在高并发场景下需要考虑乐观锁机制public void deleteDish(Long dishId) { Dish dish dishMapper.selectById(dishId); if (dish null || dish.getIsDeleted() 1) { throw new BusinessException(MessageConstant.DISH_NOT_FOUND); } // 使用版本号防止并发修改 int rows dishMapper.logicDeleteWithVersion( dishId, dish.getVersion() ); if (rows 0) { throw new ConcurrentModificationException(数据已被其他操作修改); } }对应的Mapper更新语句update idlogicDeleteWithVersion UPDATE dish SET status 0, update_time NOW(), version version 1 WHERE id #{id} AND version #{version} AND is_deleted 0 /update4.2 事务边界控制对于包含多个校验的操作需要合理设置事务边界Service RequiredArgsConstructor public class DishServiceImpl implements DishService { private final DishMapper dishMapper; private final SetmealDishMapper setmealDishMapper; // 不推荐将整个方法置于事务中 Transactional public void deleteDish(Long dishId) { // 校验逻辑 // 删除操作 } // 推荐仅对写操作加事务 public void deleteDishOptimized(Long dishId) { // 校验逻辑读操作不加事务 validateForDelete(dishId); // 仅写操作加事务 doDelete(dishId); } Transactional protected void doDelete(Long dishId) { dishMapper.logicDelete(dishId); } }5. 安全防护与性能优化5.1 SQL注入防护在使用MyBatis动态SQL时特别注意参数处理!-- 危险写法 -- select idfindDishes parameterTypestring SELECT * FROM dish WHERE name LIKE %${name}% /select !-- 安全写法 -- select idfindDishes parameterTypestring SELECT * FROM dish WHERE name LIKE CONCAT(%, #{name}, %) /select5.2 批量删除优化对于批量删除操作避免循环单条删除public void batchDeleteDishes(ListLong ids) { if (CollectionUtils.isEmpty(ids)) { return; } // 批量校验 Integer referencedCount dishMapper.countReferencedBySetmeals(ids); if (referencedCount 0) { throw new BusinessException(部分菜品被套餐引用); } // 批量更新 dishMapper.batchLogicDelete(ids); }对应的批量更新SQLupdate idbatchLogicDelete UPDATE dish SET status 0, update_time NOW() WHERE id IN foreach collectionids itemid open( separator, close) #{id} /foreach AND is_deleted 0 /update6. 实际开发中的经验总结6.1 校验顺序优化经过多个项目实践我发现校验顺序对性能有显著影响先检查是否存在select count再检查业务规则关联关系等最后执行删除操作这种顺序可以减少不必要的关联查询。例如当记录不存在时可以提前返回而不需要检查关联关系。6.2 缓存一致性处理如果系统使用了缓存删除操作后需要同步清理缓存Transactional public void deleteDish(Long dishId) { // 执行删除 dishMapper.logicDelete(dishId); // 清除缓存 redisTemplate.delete(dish: dishId); redisTemplate.delete(dish:list); }6.3 日志记录策略建议采用分级日志记录INFO级别记录成功删除操作WARN级别记录校验失败的尝试ERROR级别记录系统异常public void deleteDish(Long dishId) { try { // 业务逻辑 log.info(成功删除菜品{}, dishId); } catch (BusinessException e) { log.warn(删除菜品校验失败{}, e.getMessage()); throw e; } catch (Exception e) { log.error(删除菜品系统异常, e); throw new ServiceException(系统繁忙); } }7. 扩展思考软删除的替代方案除了status字段标记的方式还有几种常见的软删除实现方案7.1 历史表方案-- 删除时自动归档 CREATE TRIGGER archive_dish BEFORE DELETE ON dish FOR EACH ROW BEGIN INSERT INTO dish_history SELECT * FROM dish WHERE id OLD.id; END;7.2 时间戳方案TableLogic TableField(delete_time) private LocalDateTime deleteTime;7.3 多租户隔离方案ALTER TABLE dish ADD COLUMN tenant_id VARCHAR(32); -- 查询时自动过滤 SELECT * FROM dish WHERE tenant_id 当前租户 AND is_deleted 0;每种方案各有优劣需要根据具体业务场景选择。在餐饮系统中简单的status字段通常已经足够。