批量删除用foreach循环来遍历,删除被套餐关联的SQL语句比较复杂。
删除菜品表中的菜品数据这里,每次循环需要执行2次SQL,可能会出现性能问题。应该采用如下的SQL形式:delete from dish where id in (?,?,?)。
在sky-server的mapper中已有的DishMapper类中添加如下代码(负责删除菜品):
public Result save(@RequestBody DishDTO dishDTO)
这 1 行代码看起来简单,其实背后把 SpringMVC 参数解析、JSON 反序列化、校验、统一响应 全串在了一起。下面把它拆成 8 个细节逐条细说:
-
访问修饰符与返回类型
public
------ 必须 public,Spring 才能用反射调用。
Result
------ 项目里自定义的"统一响应体",通常长这样javapublic class Result<T> { private Integer code; // 1 成功 0 失败 private String msg; private T data; public static <T> Result<T> success(){ ... } }
因此前端收到的总是固定格式,不用再拼 Map。
- 方法名
save
只是方法名,和 URL 没有半毛钱关系;真正的 URL 由类上的@RequestMapping("/admin/dish")
+ 方法上的@PostMapping
拼出:
POST http://ip:port/admin/dish
-
@PostMapping
背后做了什么-
限定请求方式 = POST,其他方法(GET/PUT...)进来直接 405。
-
继承自
@RequestMapping(method = RequestMethod.POST)
,Spring 6 之后还支持@PostMapping(path = "/xxx")
自定义子路径。 -
与 REST 风格配套:新增资源 → POST;修改 → PUT;查询 → GET;删除 → DELETE。
-
-
@RequestBody
核心流程(面试常问)-
请求进来后,
DispatcherServlet
把活交给RequestResponseBodyMethodProcessor
。 -
处理器先看请求头
Content-Type: application/json
(必须是 JSON)。 -
用 Jackson (默认)把字节流读成
JsonNode
,再按字段名一一映射到DishDTO
的 setter/构造器/record 组件。 -
映射失败(类型对不上、缺失必填字段)会抛
HttpMessageNotReadableException
,Spring 默认给 400,项目里可以用@RestControllerAdvice
捕获后转Result.error(400,"JSON格式错误")
。
-
-
DishDTO
里常见字段与注解示例java@Data public class DishDTO { private Long id; // null 表示新增 @NotBlank private String name; // 菜品名称 @NotNull @Positive private BigDecimal price; // 价格 private String image; // OSS 图片 URL private String description; @Range(min = 0, max = 1) private Integer status; // 0停售 1起售 private List<DishFlavor> flavors; // 口味列表 }
可再加
@Valid
开启校验(Controller 上加@Validated
即可)。
-
事务边界
Service 层方法
saveWithFlavor
上会有@Transactional
:-
先
INSERT INTO dish
-
取到自增主键
id
-
再
INSERT INTO dish_flavor (dish_id, name, value)
VALUES (?,?,?)两步任何一步失败整体回滚,保证不会出"菜品成功、口味半截"这种脏数据。
-
-
异常流向
-
业务校验失败(如同名菜品已存在)→ 抛自定义
BusinessException
→ 被全局异常处理器捕获 → 返回Result.error(msg)
。 -
数据库唯一索引冲突 → Spring 转
DuplicateKeyException
→ 同样进全局处理器 → 前端收到 0/"菜品已存在"。 -
未捕获的运行时异常 → Spring 默认 500 → 也可被同一处理器统一包装成
Result.error(500,"系统异常")
,防止堆栈泄露。
-
-
接口文档与测试
加
@ApiOperation("新增菜品")
后,启动项目 → 访问http://ip:port/doc.html
(Knife4j)即可看到:-
请求方法:POST
-
请求体示例
{ "name": "辣子鸡", "categoryId": 3, "price": 2800, "image": "https://xxx.com/a.jpg", "description": "微辣", "status": 1, "flavors": [ { "name": "辣度", "value": "[\"微辣\",\"中辣\",\"重辣\"]" } ] }
-
响应 200:
{ "code": 1, "msg": null, "data": null }
-
一句话总结
这行代码把"HTTP 请求 → JSON → Java 对象 → 业务 → 统一响应"全链路打包好,开发者只需关心真正的业务逻辑,其余由 SpringMVC + Jackson + 统一异常处理自动完成。
DishController.java
java
/**
* 菜品管理
*/
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
@Autowired
private DishService dishService;
/**
* 新增菜品
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
dishService.saveWithFlavor(dishDTO);
return Result.success();
}
/**
* 分页查询
* @param dishPageQueryDTO
* @return
*/
@ApiOperation("菜品分页查询")
@GetMapping("/page")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
log.info("菜品分页查询:{}",dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
/**
* 批量删除
* @param ids
* @return
*/
@ApiOperation("删除菜品")
@DeleteMapping()
public Result delete(@RequestParam List<Long> ids){
log.info("批量删除菜品:{}",ids);
dishService.deleteBatch(ids);
return Result.success();
}
DishService.java
java
package com.sky.service;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.result.PageResult;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public interface DishService {
/**
* 新增菜品,同时口味
* @param dishDTO
*/
public void saveWithFlavor(DishDTO dishDTO);
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
/**
* 删除菜品
* @param ids
*/
void deleteBatch(List<Long> ids);
}
DishServiceImpl.java
java
package com.sky.service.impl;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.result.PageResult;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
@Autowired
private DishFlavorMapper dishFlavorMapper;
@Autowired
private SetmealDishMapper setmealDishMapper;
/**
* 新增菜品,同时保存对应的口味数据
*/
@Transactional
public void saveWithFlavor(DishDTO dishDTO){
log.info("新增菜品:{}",dishDTO);
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO,dish);
dishMapper.insert(dish);
Long dishId = dish.getId();
//口味
List<DishFlavor> flavors = dishDTO.getFlavors();
if(flavors != null && flavors.size() > 0){
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
}
//插入
dishFlavorMapper.insertBatch(flavors);
}
/**
* 菜品分页查询
* @param dishPageQueryDTO
* @return
*/
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
/**
* 菜品批量删除
*/
@Transactional
public void deleteBatch(List<Long> ids){
//判断是否删除是否起售中
for(Long id :ids){
Dish dish = dishMapper.getById(id);
if(dish.getStatus() == StatusConstant.ENABLE){
//起售
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//是否关联套餐
List<Long> setmealIds = setmealDishMapper.getsetmealIdsByDishIds(ids);
if(setmealIds != null && setmealIds.size() >0 ){
throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
}
//删除菜品表中数据
for(Long id : ids){
dishMapper.deleteById(id);
//删除菜品口味表中数据
dishFlavorMapper.deleteByDishId(id);
}
}
}
DishMapper.java
java
package com.sky.mapper;
import com.sky.entity.DishFlavor;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface DishFlavorMapper {
/**
* 批量插入口味
* @param flavors
*/
void insertBatch(List<DishFlavor> flavors);
/**
* 根据菜品id删除菜品口味数据
* @param dishId
*/
@Delete("delete from dish_flavor where dish_id = #{dishId}")
void deleteByDishId(Long dishId);
}
setMealDishMapper
java
public interface SetmealDishMapper {
/**
* 根据菜品id查询对应套餐id
* @param setmealDishes
*/
List<Long> getsetmealIdsByDishIds(List<Long> dishIds);
}
setMealDishMapper.xml
XML
<mapper namespace="com.sky.mapper.SetmealDishMapper">
<select id="getsetmealIdsByDishIds" resultType="java.lang.Long">
select setmeal_id from setmeal_dish where dish_id in
<foreach collection="dishIds" item ="dishId" separator="," open="(" close=")">
#{dishId}
</foreach>
</select>
</mapper>
DishMapper.xml
XML
<mapper namespace="com.sky.mapper.DishMapper">
<insert id="insert" useGeneratedKeys="true" keyProperty="id"
parameterType="com.sky.entity.Dish">
insert into dish(name, category_id, price, image, description, status, create_time, update_time, create_user, update_user)
values
(#{name},#{categoryId},#{price},#{image},#{description},#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})
</insert>
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.*,c.name as categoryName from dish d left outer join category c on c.id = d.category_id
<where>
<if test = "name != null">
and d.name like concat('%', #{name},'%')
</if>
<if test = "categoryId != null">
and d.categoryId like concat('%', #{categoryId},'%')
</if>
<if test = "status != null">
and d.status like concat('%', #{status},'%')
</if>
</where>
order by d.update_time desc
</select>
</mapper>