当前位置: 首页 > news >正文

【项目实战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 表)
字段名类型说明约束
idbigint主键 IDPRIMARY KEY
typeint分类类型(1 = 菜品,2 = 套餐)NOT NULL
namevarchar(64)分类名称NOT NULL + UNIQUE
sortint排序(越小越靠前)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;
}
  • 为什么用 DTODish 实体仅含菜品基本信息,无法接收 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:与数据库表一一对应(如 DishCategory),用于持久化。
  • DTO:数据传输对象(如 DishDto),封装前后端传输的扩展数据(多表字段)。
  • VO:视图对象,仅用于前端页面展示(按需封装数据)。
2. 事务管理关键点
  • 加在 public 方法上(private 方法不生效)。
  • 默认只回滚 RuntimeException,需回滚所有异常可配置 @Transactional(rollbackFor = Exception.class)
3. 常见问题总结
  • 多表操作数据不一致:务必加事务注解 @Transactional
  • 分页无数据:检查分页插件是否配置、页码是否从 1 开始(MP 默认)。
  • 口味未保存:未设置 dishId 或未调用 saveBatch
  • 文件操作失败:路径错误、权限不足、文件大小超限。

通过以上内容,可掌握公共字段自动填充、多表事务操作、文件上传下载等核心技能,理解 ThreadLocal、DTO、事务管理的实际应用。


瑞吉外卖 Day03+Day04 核心面试题(题目)

  1. 请详细说明瑞吉外卖项目中 “公共字段自动填充” 的实现流程,以及 ThreadLocal 在这里的核心作用是什么?如何避免 ThreadLocal 导致的内存泄漏?
  2. 瑞吉外卖中 “新增菜品” 功能涉及多表操作(dish 表和 dish_flavor 表),请说明如何保证数据一致性?DTO 在这里的作用是什么?
  3. 文件上传功能中,如何避免文件名重复导致的文件覆盖问题?SpringBoot 接收上传文件的核心对象是什么?生产环境中为什么不推荐本地存储?
  4. 分类删除功能中,如何实现 “关联菜品 / 套餐的分类不能删除” 的业务规则?全局异常处理器在这里的作用是什么?
  5. 菜品分页查询时,如何解决 “N+1 查询问题”(即查询 1 次菜品 + N 次分类)?MyBatis-Plus 的分页插件核心作用是什么?

瑞吉外卖 Day03+Day04 核心面试题(答案)

答案 1:

1. 公共字段自动填充实现流程

(1)实体类注解标记:在createTimeupdateTimecreateUserupdateUser等公共字段上添加@TableField(fill = 填充策略)注解,指定填充时机(FieldFill.INSERT表示仅插入时填充,FieldFill.INSERT_UPDATE表示插入和更新时均填充)。

(2)自定义元数据处理器:实现 MyBatis-Plus 的MetaObjectHandler接口,重写insertFillupdateFill方法。在insertFill中填充 4 个公共字段,在updateFill中填充updateTimeupdateUser字段,通过strictInsertFill/strictUpdateFill方法进行严格赋值(字段不存在时抛异常,避免拼写错误)。

(3)动态获取登录用户 ID:通过ThreadLocal封装BaseContext工具类,在登录拦截器中,当用户已登录时,将用户 ID 存入ThreadLocal;在元数据处理器中,从ThreadLocal中获取用户 ID,填充到createUserupdateUser字段。

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 表的基本字段(如namepricecategoryId),又包含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注解,专门捕获特定类型的异常(如CustomExceptionSQLIntegrityConstraintViolationException)。

(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调用呢 然后才是去深入 底层原理 和 代码细节

http://www.dtcms.com/a/553329.html

相关文章:

  • 怎么用dw做响应式网站网站主持人制作网站代言人
  • Android开发自学笔记 --- Kotlin
  • 从VB到PyCharm:编程工具跨越时代的传承与革命
  • 网站建设创新成果四年级写一小段新闻
  • 生产环境用Go语言完成微服务搭建和业务融入
  • 第九课 四川料理は辛いです
  • DevEco Studio在模拟器中改变运行的 ets 文件
  • 第5讲:项目依赖管理与资源管理
  • 网站定制案例微安电力wordpress 分类合并
  • Orleans 的异步
  • comsol livelink with matlab
  • PDF文档中表格以及形状解析-后续处理(线段生成最小多边形)
  • 5G工业边缘计算网关,重构工业智能化
  • 网站中英文切换代码wordpress插件问题
  • 解析 Lua 虚拟机整数与浮解析 Lua 虚拟机整数与浮点数处理:类型转换与运算精度控制
  • 个人网站可以做充值工业设计网页
  • 【C/C++刷题集】二叉树算法题(一)
  • Java Stream 流式编程
  • 如何进入公司网站的后台怎样用vs做简单网站
  • 长春手机建站模板wordpress搜索页
  • 消除链上气泡图:为什么换仓正在成为新的链上生存策略?
  • 什么是TRS收益互换与场外个股期权:从金融逻辑到系统开发实践
  • ARM《8》_制作linux最小根文件系统
  • IntelliJ IDEA 如何全局配置 Maven?避免每次打开新项目重新配置 (适用于 2024~2025 版本)
  • vmware17安装ubuntu2204版本qemu运行armv8处理器uboot运行调试的一些工作
  • 【开题答辩全过程】以 二手房买卖与出租系统的设计与实现为例,包含答辩的问题和答案
  • 河池市城乡住房建设厅网站一人有限公司怎么注册
  • 边缘智能的创新:MLGO微算法科技推出基于QoS感知的边缘大模型自适应拆分推理编排技术
  • 前端面试题总结
  • UE5【插件】一键重命名蓝图变量、事件、函数、宏等(实现批量翻译)