【项目实战1-瑞吉外卖|day22】
以下是瑞吉外卖day3 和day4 的重点知识总结
Day03:公共字段自动填充与分类管理
一、公共字段自动填充(核心重点)
1. 底层原理
MyBatis-Plus 通过拦截器在 SQL 执行前(INSERT/UPDATE)自动为实体类公共字段赋值,核心依赖 MetaObjectHandler 接口,通过元数据对象(MetaObject)操作实体属性。
2. 完整实现流程(含代码详解)
步骤 1:实体类注解配置
java
运行
@Data
public class Employee implements Serializable {private Long id;// 其他字段...// 插入时填充:创建时间@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime;// 插入和更新时填充:更新时间@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;// 插入时填充:创建人ID@TableField(fill = FieldFill.INSERT) private Long createUser;// 插入和更新时填充:更新人ID@TableField(fill = FieldFill.INSERT_UPDATE) private Long updateUser;
}
- 注解说明:
FieldFill.INSERT表示仅插入时填充,INSERT_UPDATE表示插入和更新时都填充。 - 注意:字段名必须与后续填充代码中的名称一致(如
createTime不能写错)。
步骤 2:自定义元数据处理器
java
运行
@Component // 必须交给Spring管理,否则MP无法扫描
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {// 插入操作填充逻辑@Overridepublic void insertFill(MetaObject metaObject) {log.info("公共字段自动填充[INSERT]");// 填充创建时间和更新时间(当前时间)this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 填充创建人和更新人(从ThreadLocal获取当前登录用户ID)this.strictInsertFill(metaObject, "createUser", Long.class, BaseContext.getCurrentId());this.strictInsertFill(metaObject, "updateUser", Long.class, BaseContext.getCurrentId());}// 更新操作填充逻辑@Overridepublic void updateFill(MetaObject metaObject) {log.info("公共字段自动填充[UPDATE]");// 填充更新时间this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());// 填充更新人this.strictUpdateFill(metaObject, "updateUser", Long.class, BaseContext.getCurrentId());}
}
- 关键方法:
strictInsertFill/strictUpdateFill是严格填充方法,字段不存在时会抛异常,避免字段名拼写错误(推荐使用)。 - 依赖:需通过
BaseContext.getCurrentId()获取当前登录用户 ID(见步骤 3)。
步骤 3:ThreadLocal 工具类(用户 ID 传递)
java
运行
public class BaseContext {// ThreadLocal存储当前线程的用户ID(线程隔离,互不干扰)private static final ThreadLocal<Long> THREAD_LOCAL = new ThreadLocal<>();// 存储用户ID到当前线程public static void setCurrentId(Long id) {THREAD_LOCAL.set(id);}// 从当前线程获取用户IDpublic static Long getCurrentId() {return THREAD_LOCAL.get();}// 移除用户ID(避免内存泄漏)public static void removeCurrentId() {THREAD_LOCAL.remove();}
}
- 核心特性:ThreadLocal 为每个线程提供独立存储副本,解决多线程下用户 ID 传递问题(如拦截器→Service→处理器的跨层数据共享)。
步骤 4:拦截器中存储用户 ID
java
运行
@WebFilter(filterName = "loginCheckFilter", urlPatterns = "/*")
public class LoginCheckFilter implements Filter {@Overridepublic void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {HttpServletRequest request = (HttpServletRequest) servletRequest;HttpServletResponse response = (HttpServletResponse) servletResponse;// 省略其他逻辑...// 已登录:将用户ID存入ThreadLocalif (request.getSession().getAttribute("employee") != null) {Long empId = (Long) request.getSession().getAttribute("employee");BaseContext.setCurrentId(empId); // 关键:存储用户IDchain.doFilter(request, response);return;}// 未登录:返回提示response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));}
}
- 时机:用户登录后,每次请求经过拦截器时,将用户 ID 存入 ThreadLocal,供后续自动填充使用。
- 清理:需在拦截器
finally块调用BaseContext.removeCurrentId(),避免线程池复用导致的内存泄漏。
3. 避坑指南
- 填充失败:检查实体类注解是否正确、处理器是否加
@Component、字段名是否与填充代码一致。 - ThreadLocal 获取不到 ID:拦截器未正确存储 ID(如登录判断逻辑错误)、未清理导致线程数据污染。
- 内存泄漏:务必在请求结束后调用
remove(),尤其是使用线程池的场景(如 Tomcat 默认线程池)。
二、分类管理 CRUD(含业务规则)
1. 数据模型(category 表)
| 字段名 | 类型 | 说明 | 约束 |
|---|---|---|---|
| id | bigint | 主键 ID | PRIMARY KEY |
| type | int | 分类类型(1 = 菜品,2 = 套餐) | NOT NULL |
| name | varchar(64) | 分类名称 | NOT NULL + UNIQUE |
| sort | int | 排序(越小越靠前) | NOT NULL DEFAULT 0 |
2. 新增分类
java
运行
// Controller
@RestController
@RequestMapping("/category")
public class CategoryController {@Autowiredprivate CategoryService categoryService;@PostMappingpublic R<String> save(@RequestBody Category category) {categoryService.save(category); // 公共字段自动填充return R.success("新增分类成功");}
}
- 核心:调用 MP 的
save方法,createTime/createUser等字段由自动填充处理器赋值。 - 唯一约束:
name字段唯一,重复添加会触发 SQL 异常,由全局异常处理器捕获(见删除部分)。
3. 分页查询分类
java
运行
@GetMapping("/page")
public R<Page<Category>> page(int page, int pageSize) {// 1. 创建分页对象Page<Category> pageInfo = new Page<>(page, pageSize);// 2. 条件构造器:按sort升序,updateTime降序LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);// 3. 执行分页查询categoryService.page(pageInfo, queryWrapper);return R.success(pageInfo);
}
- 分页插件依赖:需在配置类中注册分页插件,否则分页无效:
java
运行
@Configuration public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 分页插件return interceptor;} }
预埋link:MP知识点
4. 删除分类(关联校验 + 异常处理)
步骤 1:自定义业务异常
java
运行
// 自定义异常:用于业务规则违规(如关联数据不能删除)
public class CustomException extends RuntimeException {public CustomException(String message) {super(message);}
}
步骤 2:全局异常处理器
java
运行
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {// 处理SQL唯一约束异常(如分类名称重复)@ExceptionHandler(SQLIntegrityConstraintViolationException.class)public R<String> handleSqlException(SQLIntegrityConstraintViolationException ex) {if (ex.getMessage().contains("Duplicate entry")) {String name = ex.getMessage().split("'")[1];return R.error(name + "已存在");}return R.error("数据库异常");}// 处理自定义业务异常(如关联数据不能删除)@ExceptionHandler(CustomException.class)public R<String> handleCustomException(CustomException ex) {return R.error(ex.getMessage());}
}
步骤 3:删除逻辑(含关联校验)
java
运行
// ServiceImpl
@Service
public class CategoryServiceImpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {@Autowiredprivate DishService dishService;@Autowiredprivate SetmealService setmealService;@Overridepublic void remove(Long id) {// 1. 检查是否关联菜品LambdaQueryWrapper<Dish> dishQueryWrapper = new LambdaQueryWrapper<>();dishQueryWrapper.eq(Dish::getCategoryId, id);long dishCount = dishService.count(dishQueryWrapper);if (dishCount > 0) {throw new CustomException("当前分类关联菜品,不能删除");}// 2. 检查是否关联套餐LambdaQueryWrapper<Setmeal> setmealQueryWrapper = new LambdaQueryWrapper<>();setmealQueryWrapper.eq(Setmeal::getCategoryId, id);long setmealCount = setmealService.count(setmealQueryWrapper);if (setmealCount > 0) {throw new CustomException("当前分类关联套餐,不能删除");}// 3. 无关联:执行删除super.removeById(id);}
}
步骤 4:Controller 调用
java
运行
@DeleteMapping
public R<String> delete(Long id) {categoryService.remove(id); // 调用自定义删除方法return R.success("删除成功");
}
5. 修改分类
java
运行
@PutMapping
public R<String> update(@RequestBody Category category) {categoryService.updateById(category); // updateTime/updateUser自动填充return R.success("修改成功");
}
- 注意:前端需传递完整实体(含 id),MP 的
updateById会根据 id 更新其他字段。
Day04:文件上传下载与菜品管理
一、文件上传下载
1. 底层原理
- 文件上传:前端通过
multipart/form-data格式提交,后端用MultipartFile接收,临时文件需转存到指定目录(否则请求结束后删除)。 - 文件下载:后端通过输入流读取文件,输出流写入响应体,浏览器根据
Content-Type决定预览或下载。
2. 完整代码实现
步骤 1:配置文件(application.yml)
yaml
reggie:path: D:\reggie_img\ # 文件存储路径
spring:servlet:multipart:max-file-size: 10MB # 单个文件最大限制max-request-size: 100MB # 单次请求最大限制
步骤 2:文件上传下载控制器
java
运行
@RestController
@RequestMapping("/common")
@Slf4j
public class CommonController {@Value("${reggie.path}") // 注入存储路径private String basePath;// 文件上传@PostMapping("/upload")public R<String> upload(MultipartFile file) {// 1. 获取原始文件名,提取后缀(如.jpg)String originalFilename = file.getOriginalFilename();String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));// 2. 生成UUID文件名(避免重复覆盖)String fileName = UUID.randomUUID().toString() + suffix;// 3. 创建目录(不存在则创建)File dir = new File(basePath);if (!dir.exists()) {dir.mkdirs(); // 多级目录用mkdirs()}try {// 4. 转存临时文件到指定路径file.transferTo(new File(basePath + fileName));} catch (IOException e) {log.error("上传失败", e);return R.error("上传失败");}return R.success(fileName); // 返回文件名(用于后续下载)}// 文件下载@GetMapping("/download")public void download(String name, HttpServletResponse response) {try {// 1. 输入流:读取服务器文件FileInputStream fis = new FileInputStream(new File(basePath + name));// 2. 输出流:写入浏览器ServletOutputStream sos = response.getOutputStream();// 3. 设置响应头:图片预览response.setContentType("image/jpeg");// 4. 缓冲读写byte[] buffer = new byte[1024];int len;while ((len = fis.read(buffer)) != -1) {sos.write(buffer, 0, len);sos.flush();}// 5. 关闭资源sos.close();fis.close();} catch (Exception e) {log.error("下载失败", e);}}
}
3. 避坑指南
- 文件过大:需配置
max-file-size和max-request-size,默认值太小(1MB)。 - 路径错误:检查
basePath是否正确(如多斜杠/、路径不存在),打印basePath + fileName排查。 - 临时文件删除:必须调用
transferTo转存,否则临时文件会被自动删除。 - 跨域问题:前后端域名不同时,需配置跨域允许(见扩展)。
二、菜品管理(多表操作 + DTO + 事务)
1. 核心业务逻辑
- 涉及表:
dish(菜品基本信息)、dish_flavor(菜品口味,通过dish_id关联)。 - 难点:前端传递 “菜品 + 口味列表” 需用 DTO 接收,两表操作需事务保证原子性。
2. DTO 设计(DishDto)
java
运行
@Data
public class DishDto extends Dish {// 扩展:接收前端传递的口味列表private List<DishFlavor> flavors = new ArrayList<>();// 分页展示用:分类名称private String categoryName;
}
- 为什么用 DTO:
Dish实体仅含菜品基本信息,无法接收flavors字段,DTO 用于扩展传输数据。
3. 新增菜品(含口味)
步骤 1:Service 层(事务控制)
java
运行
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {@Autowiredprivate DishFlavorService dishFlavorService;// 新增菜品+保存口味(事务控制)@Override@Transactional // 保证两表操作原子性public void saveWithFlavor(DishDto dishDto) {// 1. 保存菜品基本信息到dish表this.save(dishDto);Long dishId = dishDto.getId(); // 保存后自动生成的菜品ID// 2. 为口味列表设置dishId(关联菜品)List<DishFlavor> flavors = dishDto.getFlavors();flavors = flavors.stream().map(flavor -> {flavor.setDishId(dishId);return flavor;}).collect(Collectors.toList());// 3. 批量保存口味到dish_flavor表dishFlavorService.saveBatch(flavors);}
}
- 事务注解:
@Transactional需配合启动类的@EnableTransactionManagement生效。
步骤 2:Controller 层
java
运行
@RestController
@RequestMapping("/dish")
public class DishController {@Autowiredprivate DishService dishService;@PostMappingpublic R<String> save(@RequestBody DishDto dishDto) {dishService.saveWithFlavor(dishDto);return R.success("新增菜品成功");}
}
4. 菜品分页查询(含分类名称)
java
运行
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {// 1. 菜品分页查询Page<Dish> dishPage = new Page<>(page, pageSize);LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();queryWrapper.like(name != null, Dish::getName, name).orderByDesc(Dish::getUpdateTime);dishService.page(dishPage, queryWrapper);// 2. 转换为DishDto分页(补充分类名称)Page<DishDto> dishDtoPage = new Page<>();// 拷贝分页基本属性(总条数、页码等)BeanUtils.copyProperties(dishPage, dishDtoPage, "records");// 3. 处理菜品列表:补充分类名称List<Dish> dishList = dishPage.getRecords();List<DishDto> dishDtoList = dishList.stream().map(dish -> {DishDto dishDto = new DishDto();BeanUtils.copyProperties(dish, dishDto);// 根据categoryId查询分类名称Long categoryId = dish.getCategoryId();Category category = categoryService.getById(categoryId);if (category != null) {dishDto.setCategoryName(category.getName());}return dishDto;}).collect(Collectors.toList());dishDtoPage.setRecords(dishDtoList);return R.success(dishDtoPage);
}
- 优化点:避免 N+1 查询(每条菜品查一次分类),可提前缓存所有分类到 Map 中。
5. 菜品修改(先删后加策略)
步骤 1:数据回显(查询菜品 + 口味)
java
运行
// Service
@Override
public DishDto getByIdWithFlavor(Long id) {// 1. 查询菜品基本信息Dish dish = this.getById(id);DishDto dishDto = new DishDto();BeanUtils.copyProperties(dish, dishDto);// 2. 查询关联口味List<DishFlavor> flavors = dishFlavorService.list(new LambdaQueryWrapper<DishFlavor>().eq(DishFlavor::getDishId, id));dishDto.setFlavors(flavors);return dishDto;
}// Controller
@GetMapping("/{id}")
public R<DishDto> getById(@PathVariable Long id) {return R.success(dishService.getByIdWithFlavor(id));
}
步骤 2:保存修改(更新菜品 + 口味)
java
运行
// Service
@Override
@Transactional
public void updateWithFlavor(DishDto dishDto) {// 1. 更新菜品基本信息this.updateById(dishDto);// 2. 删除原有口味(先删后加,避免残留)dishFlavorService.remove(new LambdaQueryWrapper<DishFlavor>().eq(DishFlavor::getDishId, dishDto.getId()));// 3. 新增修改后的口味(同新增逻辑)List<DishFlavor> flavors = dishDto.getFlavors();flavors.forEach(flavor -> flavor.setDishId(dishDto.getId()));dishFlavorService.saveBatch(flavors);
}// Controller
@PutMapping
public R<String> update(@RequestBody DishDto dishDto) {dishService.updateWithFlavor(dishDto);return R.success("修改成功");
}
- 先删后加优势:无需判断口味是新增 / 修改 / 删除,逻辑简单,适合小数据量场景。
三、核心拓展与总结
1. DTO/Entity/VO 区别
- Entity:与数据库表一一对应(如
Dish、Category),用于持久化。 - DTO:数据传输对象(如
DishDto),封装前后端传输的扩展数据(多表字段)。 - VO:视图对象,仅用于前端页面展示(按需封装数据)。
2. 事务管理关键点
- 加在 public 方法上(private 方法不生效)。
- 默认只回滚 RuntimeException,需回滚所有异常可配置
@Transactional(rollbackFor = Exception.class)。
3. 常见问题总结
- 多表操作数据不一致:务必加事务注解
@Transactional。 - 分页无数据:检查分页插件是否配置、页码是否从 1 开始(MP 默认)。
- 口味未保存:未设置
dishId或未调用saveBatch。 - 文件操作失败:路径错误、权限不足、文件大小超限。
通过以上内容,可掌握公共字段自动填充、多表事务操作、文件上传下载等核心技能,理解 ThreadLocal、DTO、事务管理的实际应用。
瑞吉外卖 Day03+Day04 核心面试题(题目)
- 请详细说明瑞吉外卖项目中 “公共字段自动填充” 的实现流程,以及 ThreadLocal 在这里的核心作用是什么?如何避免 ThreadLocal 导致的内存泄漏?
- 瑞吉外卖中 “新增菜品” 功能涉及多表操作(dish 表和 dish_flavor 表),请说明如何保证数据一致性?DTO 在这里的作用是什么?
- 文件上传功能中,如何避免文件名重复导致的文件覆盖问题?SpringBoot 接收上传文件的核心对象是什么?生产环境中为什么不推荐本地存储?
- 分类删除功能中,如何实现 “关联菜品 / 套餐的分类不能删除” 的业务规则?全局异常处理器在这里的作用是什么?
- 菜品分页查询时,如何解决 “N+1 查询问题”(即查询 1 次菜品 + N 次分类)?MyBatis-Plus 的分页插件核心作用是什么?
瑞吉外卖 Day03+Day04 核心面试题(答案)
答案 1:
1. 公共字段自动填充实现流程
(1)实体类注解标记:在createTime、updateTime、createUser、updateUser等公共字段上添加@TableField(fill = 填充策略)注解,指定填充时机(FieldFill.INSERT表示仅插入时填充,FieldFill.INSERT_UPDATE表示插入和更新时均填充)。
(2)自定义元数据处理器:实现 MyBatis-Plus 的MetaObjectHandler接口,重写insertFill和updateFill方法。在insertFill中填充 4 个公共字段,在updateFill中填充updateTime和updateUser字段,通过strictInsertFill/strictUpdateFill方法进行严格赋值(字段不存在时抛异常,避免拼写错误)。
(3)动态获取登录用户 ID:通过ThreadLocal封装BaseContext工具类,在登录拦截器中,当用户已登录时,将用户 ID 存入ThreadLocal;在元数据处理器中,从ThreadLocal中获取用户 ID,填充到createUser和updateUser字段。
2. ThreadLocal 的核心作用
- 解决跨组件数据共享:元数据处理器无法直接获取 HttpSession,而同一 HTTP 请求对应的线程 ID 唯一,ThreadLocal 为每个线程提供独立的存储副本,实现 “拦截器存 ID→处理器取 ID” 的跨层数据传递,无需手动在方法间传递参数。
- 保证线程安全:多线程环境下,不同请求的用户 ID 存储在各自线程的副本中,互不干扰,避免数据污染。
3. 避免 ThreadLocal 内存泄漏的方案
- 内存泄漏原因:ThreadLocal 的
Entry中 Key 是弱引用,但 ThreadLocalMap 与 Thread 的生命周期绑定。若使用线程池(如 Tomcat 默认线程池),线程会被复用,未清理的Entry会一直占用内存,导致内存泄漏。 - 解决方案:在请求处理结束后,调用
ThreadLocal.remove()方法清理当前线程的存储数据。通常在拦截器的finally块中添加BaseContext.removeCurrentId(),确保无论请求成功与否,都能清理数据。
答案 2:
1. 保证多表操作数据一致性的方案:事务控制
(1)添加事务注解:在 Service 层的saveWithFlavor方法(同时操作 dish 表和 dish_flavor 表)上添加@Transactional注解,确保两个数据库操作具备原子性 —— 要么都执行成功,要么都回滚,避免部分操作成功导致的数据不一致。
(2)启动事务支持:在 SpringBoot 引导类上添加@EnableTransactionManagement注解,开启 Spring 对声明式事务的管理,使@Transactional注解生效。
(3)配置异常回滚规则:Spring 事务默认仅对RuntimeException及其子类触发回滚。若需对 CheckedException(如 IOExcepiton)也触发回滚,可配置@Transactional(rollbackFor = Exception.class),明确指定回滚的异常类型。
2. DTO 的核心作用
- 问题背景:前端提交的新增菜品请求参数,既包含 dish 表的基本字段(如
name、price、categoryId),又包含flavors(菜品口味列表,对应 dish_flavor 表数据)。而Dish实体类仅与 dish 表字段一一对应,无法接收flavors字段,导致参数无法正确封装。 - 核心作用:
DishDto继承Dish类,扩展List<DishFlavor> flavors字段,专门用于前后端数据传输。它能一次性接收前端传递的多表关联参数,避免在 Controller 中手动拆分参数,简化代码;同时隔离了传输层数据与持久层实体,避免实体类因传输需求冗余不必要的字段。
答案 3:
1. 避免文件名重复导致文件覆盖的方案
- 核心思路:生成全局唯一的文件名,确保不同用户上传的同名文件不会覆盖。
- 实现方式:通过
UUID.randomUUID().toString()生成 32 位唯一字符串,拼接原始文件的后缀名(如.jpg、.png),构成最终的文件名。示例代码:java
运行
// 获取原始文件名后缀 String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); // 生成唯一文件名 String fileName = UUID.randomUUID().toString() + suffix;
2. SpringBoot 接收上传文件的核心对象:MultipartFile
MultipartFile是 SpringBoot 对 Apache Commons FileUpload 组件的封装,专门用于接收前端通过multipart/form-data格式提交的文件数据。- 核心特性:通过该对象可获取文件的原始文件名(
getOriginalFilename())、文件大小(getSize())、文件输入流(getInputStream())等信息;同时,上传的文件会被临时存储在服务器的临时目录(如/tmp/tomcat),需通过transferTo(File dest)方法转存到指定目录,否则请求结束后临时文件会被自动删除。
3. 生产环境不推荐本地存储的原因
(1)容量限制:服务器磁盘空间有限,无法支撑大量文件(如菜品图片、用户头像)的长期存储,容易出现磁盘满的问题。
(2)迁移困难:当服务器需要迁移或扩容时,需手动拷贝本地存储的文件,操作复杂且容易丢失数据。
(3)高可用缺失:本地存储是单点存储,若服务器故障或磁盘损坏,文件数据会永久丢失,无备份和容错机制。
(4)访问效率低:无法支持 CDN 加速,异地用户访问文件时延迟较高,影响用户体验。
- 推荐方案:使用云存储服务(如阿里云 OSS、腾讯云 COS),通过 SDK 将文件上传到云服务器,返回文件的公网 URL。云存储支持高容量、高可用、异地备份和 CDN 加速,更适合生产环境。
答案 4:
1. “关联数据的分类不能删除” 的业务规则实现步骤
(1)关联数据校验:在删除分类前,分别查询 dish 表和 setmeal 表,统计当前分类 ID 对应的关联数据数量。
- 校验关联菜品:通过
LambdaQueryWrapper构建条件(dish.category_id = 分类ID),调用dishService.count()统计数量,若数量 > 0,说明关联了菜品。 - 校验关联套餐:同理,查询 setmeal 表中
setmeal.category_id = 分类ID的记录数量,若数量 > 0,说明关联了套餐。示例代码:
java
运行
// 校验关联菜品
LambdaQueryWrapper<Dish> dishQueryWrapper = new LambdaQueryWrapper<>();
dishQueryWrapper.eq(Dish::getCategoryId, id);
long dishCount = dishService.count(dishQueryWrapper);
if (dishCount > 0) {throw new CustomException("当前分类关联菜品,不能删除");
}
(2)抛出自定义业务异常:创建CustomException类(继承RuntimeException),当检测到关联数据时,抛出该异常并携带具体的业务提示信息(如 “当前分类关联套餐,不能删除”)。(3)执行删除操作:若未检测到关联数据,调用super.removeById(id)(继承自 MyBatis-Plus 的ServiceImpl)执行分类删除。
2. 全局异常处理器的作用
(1)统一捕获异常:通过@ControllerAdvice注解指定拦截所有@RestController和@Controller注解的类,配合@ExceptionHandler注解,专门捕获特定类型的异常(如CustomException、SQLIntegrityConstraintViolationException)。
(2)返回友好响应:捕获异常后,将异常信息封装为项目统一的返回结果类R(如R.error(异常提示信息)),避免直接向前端返回堆栈信息,提升用户体验,同时保证响应格式统一。
(3)简化异常处理逻辑:无需在每个 Controller 或 Service 方法中手动 try-catch 捕获业务异常,集中管理所有异常处理逻辑,降低代码冗余,便于维护。示例代码(全局异常处理器核心逻辑):
java
运行
@ExceptionHandler(CustomException.class)
public R<String> handleCustomException(CustomException ex) {log.error("业务异常:{}", ex.getMessage());return R.error(ex.getMessage());
}
答案 5:
1. 解决分页查询 N+1 问题的方案
- N+1 问题描述:分页查询菜品时,先执行 1 次 SQL 查询所有菜品(N 条记录),再循环 N 次执行 SQL 查询每条菜品对应的分类名称,导致数据库查询次数过多,性能低下。
- 优化思路:提前缓存所有需要的分类数据,减少数据库查询次数,将 N+1 次查询优化为 2 次查询(1 次查菜品 + 1 次查分类)。
- 实现步骤:
- (1)一次性查询所有菜品分类(
type=1),将分类 ID 和分类名称存入Map<Long, String>(key 为分类 ID,value 为分类名称)。 - (2)遍历分页查询得到的菜品列表,从 Map 中直接获取分类名称,无需再次查询数据库。示例代码:
java
运行
// 1. 缓存所有菜品分类到Map Map<Long, String> categoryMap = categoryService.list(new LambdaQueryWrapper<Category>().eq(Category::getType, 1) ).stream().collect(Collectors.toMap(Category::getId, Category::getName));// 2. 从Map中获取分类名称 List<DishDto> dishDtoList = dishList.stream().map(dish -> {DishDto dishDto = new DishDto();BeanUtils.copyProperties(dish, dishDto);// 直接从Map获取,无需查询数据库dishDto.setCategoryName(categoryMap.get(dish.getCategoryId()));return dishDto; }).collect(Collectors.toList());
2. MyBatis-Plus 分页插件的核心作用
(1)自动改写 SQL 语句:拦截分页查询请求(如调用service.page(Page, QueryWrapper)方法),在原始 SQL 末尾自动添加分页关键字(如 MySQL 的LIMIT),无需手动编写分页 SQL,简化开发。
(2)封装分页结果:将查询结果封装到 MyBatis-Plus 的Page对象中,该对象包含分页核心信息:总记录数(total)、总页数(pages)、当前页码(current)、每页条数(size)、当前页数据列表(records)等,直接返回给前端即可满足分页展示需求。
(3)支持多数据库适配:插件内部已适配 MySQL、Oracle、SQL Server 等多种数据库的分页语法,无需根据数据库类型调整分页逻辑。
- 使用前提:需在配置类中注册分页插件,否则分页功能不生效,配置代码如下:
java
运行
@Configuration public class MybatisPlusConfig {@Beanpublic MybatisPlusInterceptor mybatisPlusInterceptor() {MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();// 添加分页插件interceptor.addInnerInterceptor(new PaginationInnerInterceptor());return interceptor;} }
先专注业务逻辑 和API调用呢 然后才是去深入 底层原理 和 代码细节
