苍穹外卖优化过程遇到的问题
1、使用mybatis-plus时,推断表名的默认规则:
是否使用 @TableName | 表名推断规则 | 举例 |
---|---|---|
未使用 | 实体类名驼峰转下划线 | UserInfo → user_info |
使用 | 显式指定表名 | @TableName("t_user_info") → t_user_info |
2、@TableId注解,如果不使用会怎么样
不写
@TableId
,只要实体类里有id
字段,就能正常识别为主键。若类里没有叫id
的属性,则启动时会报错默认生成策略是
NONE
,不会自动赋值,需要你自己在插入前给id
设值,或者让数据库自增并在表结构里配置AUTO_INCREMENT
。
3、有关两种wrapper的不同区别:
4、lambdaQueryWrapper的lambda表达式需要指定泛型
LambdaQueryWrapper<CategoryPO> wrapper = new LambdaQueryWrapper<>().like(name!=null,CategoryPO::getName,name).eq(type!=null, CategoryPO::getType,type);
// CategoryPO::getName 报错,”静态字段不能调用非静态方法“
LambdaQueryWrapper<CategoryPO> wrapper = new LambdaQueryWrapper<CategoryPO>().like(name!=null,CategoryPO::getName,name).eq(type!=null, CategoryPO::getType,type);
5、mybatis-plus的ServiceImpl的常用函数有哪些,整理成表格
函数分类 | 函数签名 | 功能描述 | 典型使用场景 |
---|---|---|---|
单条查询 | T getById(Serializable id) | 根据主键 ID 查询单条数据,内部调用 BaseMapper.selectById ,返回实体对象 | 根据用户 ID 查询用户详情(如 userService.getById(1001) ) |
T getOne(Wrapper<T> queryWrapper) | 根据条件构造器查询单条数据,默认查询到多条时抛 TooManyResultsException | 根据手机号查询唯一用户(如 wrapper.eq(User::getPhone, "138xxxx8888") ) | |
T getOne(Wrapper<T> queryWrapper, boolean throwEx) | 重载方法:throwEx=false 时,查询到多条数据不抛异常,返回第一条 | 允许 “匹配多条时取第一条” 的场景(如查询 “最新创建的一条测试订单”) | |
Object getObj(Wrapper<T> queryWrapper) | 根据条件构造器查询 “单个字段值”,内部调用 BaseMapper.selectObj | 根据用户 ID 查询用户名(如 wrapper.eq(User::getId, 1001).select(User::getUsername) ) | |
批量查询 | List<T> listByIds(Collection<? extends Serializable> idList) | 根据主键 ID 集合批量查询,内部调用 BaseMapper.selectBatchIds | 批量查询多个用户(如 idList = Arrays.asList(1001, 1002, 1003) ) |
List<T> list(Wrapper<T> queryWrapper) | 根据条件构造器查询数据列表,无匹配结果返回空列表(非 null) | 查询 “状态为正常的所有商品”(如 wrapper.eq(Goods::getStatus, 1) ) | |
List<Map<String, Object>> listMaps(Wrapper<T> queryWrapper) | 根据条件构造器查询 “字段名 - 值” 的 Map 列表,无需实体类映射 | 统计 “各分类的商品数量”(如 wrapper.groupBy(Goods::getCategoryId).select(Goods::getCategoryId, "count(*) as num") ) | |
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper) | 分页查询:结合分页参数和条件构造器,返回分页结果(需配合分页插件) | 分页查询 “用户列表”(如 new Page<>(1, 10) 表示第 1 页,每页 10 条) | |
IPage<Map<String, Object>> pageMaps(IPage<T> page, Wrapper<T> queryWrapper) | 分页查询并返回 Map 列表,适合无需实体类的场景 | 分页统计 “各店铺的订单量”(避免创建临时实体类) | |
数量查询 | long count(Wrapper<T> queryWrapper) | 根据条件构造器查询符合条件的记录总数,返回长整型(避免溢出) | 统计 “已完成的订单数量”(如 wrapper.eq(Order::getStatus, 3) ) |
单条操作 | boolean save(T entity) | 插入单条实体数据,内部调用 BaseMapper.insert ,返回布尔值表示成功与否 | 新增用户(如 userService.save(new User("张三", "138xxxx8888")) ) |
boolean updateById(T entity) | 根据主键 ID 更新实体数据(只更非 null 字段),返回布尔值 | 修改用户手机号(如 user.setId(1001); user.setPhone("139xxxx9999"); userService.updateById(user) ) | |
boolean removeById(Serializable id) | 根据主键 ID 删除单条数据,返回布尔值表示成功与否 | 删除指定 ID 的订单(如 orderService.removeById(2001) ) | |
批量操作 | boolean saveBatch(Collection<T> entityList) | 批量插入实体列表,默认批次大小为 1000(可通过配置修改) | 批量导入商品数据(一次插入 500 条商品记录) |
boolean saveBatch(Collection<T> entityList, int batchSize) | 重载方法:手动指定批次大小(如每次插入 200 条,避免数据库压力过大) | 批量导入大量数据(如 10000 条用户数据,分 50 批插入) | |
boolean updateBatchById(Collection<T> entityList) | 批量根据主键更新实体数据(只更非 null 字段) | 批量修改商品库存(如批量将 100 个商品的库存 + 10) | |
boolean updateBatchById(Collection<T> entityList, int batchSize) | 重载方法:手动指定更新批次大小 | 批量更新大量数据时控制批次(如 2000 条订单状态更新,分 20 批处理) | |
boolean removeByIds(Collection<? extends Serializable> idList) | 根据主键 ID 集合批量删除数据,返回布尔值 | 批量删除多个商品(如 idList = Arrays.asList(3001, 3002) ) | |
条件操作 | boolean update(Wrapper<T> updateWrapper) | 根据条件构造器更新数据(需在 updateWrapper 中设置更新字段) | 将 “库存不足 10 的商品状态改为下架”(updateWrapper.lt(Goods::getStock, 10).set(Goods::getStatus, 0) ) |
boolean update(T entity, Wrapper<T> updateWrapper) | 结合实体和条件构造器更新:entity 存更新值,wrapper 存筛选条件 | 给 “店铺 ID=1001 的所有商品打 9 折”(entity.setPrice(price*0.9) ,wrapper.eq(Goods::getShopId, 1001) ) | |
boolean remove(Wrapper<T> queryWrapper) | 根据条件构造器删除数据,返回布尔值表示成功与否 | 删除 “已过期的优惠券”(如 wrapper.lt(Coupon::getEndTime, new Date()) ) | |
其他常用 | boolean exists(Wrapper<T> queryWrapper) | 判断是否存在符合条件的记录,返回布尔值(存在为 true,不存在为 false) | 校验 “手机号是否已被注册”(如 wrapper.eq(User::getPhone, "138xxxx8888") ) |
<R> List<R> listObjs(Wrapper<T> queryWrapper, Function<? super Object, R> mapper) | 查询指定字段列表并转换类型(如将 Integer 类型的 ID 转为 String) | 查询 “所有商品 ID 并转为 String 列表”(listObjs(wrapper.select(Goods::getId), Object::toString) ) |
6、mybatis-plus的BaseMapper的常用函数有哪些,整理成表格
函数分类 | 函数签名 | 功能描述 | 典型使用场景 |
---|---|---|---|
单条查询 | T selectById(Serializable id) | 根据主键 ID 查询单条数据,返回对应的实体对象 | 根据用户 ID 查询用户详情(如 userMapper.selectById(1001) ) |
T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) | 根据条件构造器 queryWrapper 查询单条数据,若有多条结果则抛异常 | 根据手机号查询唯一用户(如 wrapper.eq(User::getPhone, "138xxxx8888") ) | |
Object selectObj(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) | 根据条件构造器查询 “单个字段值”(返回第一个字段的结果,需强转类型) | 根据用户 ID 查询用户名(如 wrapper.eq(User::getId, 1001).select(User::getUsername) ) | |
批量查询 | List<T> selectBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList) | 根据主键 ID 集合批量查询数据,返回实体列表 | 批量查询多个用户(如 idList = Arrays.asList(1001, 1002, 1003) ) |
List<T> selectList(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) | 根据条件构造器查询数据列表,无匹配结果则返回空列表(非 null) | 查询 “状态为正常的所有商品”(如 wrapper.eq(Goods::getStatus, 1) ) | |
List<Map<String, Object>> selectMaps(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) | 根据条件构造器查询数据,返回 “字段名 - 值” 的 Map 列表(无需实体类映射) | 统计 “各分类的商品数量”(如 wrapper.groupBy(Goods::getCategoryId).select(Goods::getCategoryId, "count(*) as num") ) | |
IPage<T> selectPage(IPage<T> page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper) | 分页查询:根据条件构造器和分页参数(页码、每页条数)返回分页结果 | 分页查询 “用户列表”(如 new Page<>(1, 10) 表示第 1 页,每页 10 条) | |
数量查询 | Long selectCount(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) | 根据条件构造器查询符合条件的记录总数,返回长整型(避免溢出) | 统计 “已完成的订单数量”(如 wrapper.eq(Order::getStatus, 3) ) |
单条插入 | int insert(T entity) | 插入单条实体数据,返回 “影响行数”(成功为 1,失败为 0);支持主键自动生成 | 新增用户(如 userMapper.insert(new User("张三", "138xxxx8888")) ) |
批量插入 | int insertBatchSomeColumn(List<T> entityList) | 批量插入实体列表,自动忽略 null 字段(需 MP 3.3.0+ 版本,非原生 BaseMapper 方法,需配置插件) | 批量导入商品数据(一次插入 100 条商品记录) |
单条更新 | int updateById(@Param(Constants.ENTITY) T entity) | 根据主键 ID 更新实体数据(只更新非 null 字段),返回影响行数 | 修改用户手机号(如 user.setId(1001); user.setPhone("139xxxx9999"); userMapper.updateById(user) ) |
int update(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper) | 根据条件构造器更新实体数据(entity 存更新值,wrapper 存筛选条件) | 批量将 “库存不足 10 的商品状态改为下架”(entity.setStatus (0),wrapper.lt (Goods::getStock, 10)) | |
单条删除 | int deleteById(Serializable id) | 根据主键 ID 删除单条数据,返回影响行数 | 删除指定 ID 的订单(如 orderMapper.deleteById(2001) ) |
批量删除 | int deleteBatchIds(@Param(Constants.COLLECTION) Collection<? extends Serializable> idList) | 根据主键 ID 集合批量删除数据,返回影响行数 | 批量删除多个商品(如 idList = Arrays.asList(3001, 3002) ) |
int delete(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper) | 根据条件构造器删除数据,返回影响行数 | 删除 “已过期的优惠券”(如 wrapper.lt(Coupon::getEndTime, new Date()) ) |
7、mybatis-plus更新时,只更新新实体的非null字段吗
需求场景 | 推荐方案 | 优点 | 注意事项 |
---|---|---|---|
仅更新部分非 null 字段(常规场景) | updateById(T entity) | 简洁高效,避免误置空 | 必须传主键,null 字段不更新 |
主动更新部分字段为 null | update(null, UpdateWrapper) | 灵活控制,仅更新指定字段 | 需手动 set 要更新的字段 |
全量同步实体(包括 null 字段) | 方案 1:update + UpdateWrapper 逐个 set 字段 | 安全可控,仅影响当前更新 | 字段多时代码繁琐 |
全量同步为常态(特殊场景) | 方案 2:配置 field-strategy: IGNORED | 无需改代码,updateById 直接全量更新 | 全局影响,易误操作其他更新 |
8、MyBatis-Plus 自动填充功能的触发条件
MyBatis-Plus 的 @TableField(fill = FieldFill.INSERT_UPDATE)
自动填充,仅在 “通过实体对象(T entity)更新” 时触发,而当你使用 update(UpdateWrapper)
方式时,这种方式不依赖实体对象,因此不会触发字段自动填充。
9、SpringCache的底层及工作原理
核心问题分类 | 具体问题描述 | 核心结论 | 底层逻辑 / 企业实践细节 |
---|---|---|---|
SpringCache 基础 | SpringCache 底层及工作原理是什么? | 是 “缓存抽象层”,不直接实现缓存,通过 CacheManager 对接具体缓存(Redis/Caffeine 等),用注解简化缓存操作。 | 1. 底层流程: - 开启 @EnableCaching 后,Spring 扫描注解生成代理类;- 调用注解方法时,代理类先查缓存:命中则返回,未命中则执行方法 + 存缓存; - 支持 @Cacheable (查)、@CachePut (更)、@CacheEvict (删)等注解;2. 核心组件: - CacheManager :管理缓存实例(如 RedisCacheManager 对接 Redis);- Cache :单个缓存实例(对应 cacheNames ,如 cacheNames="category" 是一个 Cache)。 |
SpringCache 存储位置 | @CacheEvict(cacheNames="category") 的缓存存到哪里?如何指定? | 存储位置由 CacheManager 决定,默认存内存,配置后可存 Redis/Caffeine 等;需配置对应 CacheManager 。 | 1. 默认:无配置时用 ConcurrentMapCacheManager ,缓存存 JVM 内存(仅单服务可用,重启丢失);2. 企业实践: - 配置 RedisCacheManager 存 Redis(分布式场景必用);- 需引入 spring-boot-starter-data-redis 依赖,配置 Redis 地址、序列化方式(如 GenericJackson2JsonRedisSerializer )。 |
@CacheEvict 与手动清理 | Controller 中 clearCache(pattern) 是否能删除 @CacheEvict 对应的缓存? | 能,但需 pattern 匹配 @CacheEvict 生成的 Redis Key;否则不能。 | 1. Key 格式:@CacheEvict 生成的 Key 通常为「前缀 + cacheNames+::+key」(如 myapp:category::1001 );2. 匹配规则: - 若 pattern 为 myapp:category::* (Redis 通配符),则删除 category 缓存名下所有数据;- 若 pattern 不匹配(如 myapp:order::* ),则不删除。 |
List 转数组 | toArray(new DishFlavorPO[0]) 中参数的作用是什么? | 参数是 “类型标识 + 控制数组长度”,确保转成目标类型数组,避免内存浪费。 | 1. 作用 1:传递类型(告诉 toArray 转成 DishFlavorPO[] ,而非 Object[] );2. 作用 2:控制长度: - 若参数数组长度 < List 元素数, toArray 新建 “刚好容纳元素” 的数组;- 企业推荐:JDK8 用 new DishFlavorPO[0] ,JDK11 + 用 DishFlavorPO[]::new (更简洁)。 |
Service 层依赖注入 | Service 层是否要注入其他表的 Service,而非 Mapper? | 是,遵循分层架构,解耦依赖、复用逻辑、便于事务控制。 | 1. 分层原则: - Service 层负责业务逻辑,依赖 “其他业务能力(Service)”; - Mapper 层仅负责数据访问,不跨层暴露; 2. 企业实践: - 注入 Service 接口(如 CategoryService ),而非实现类;- 避免直接注入其他表 Mapper,减少代码冗余(如复用 categoryService.getCategoryNameById() 中的空值防护逻辑)。 |
MyBatis-Plus 批量插入 | DishFlavorMapper 调用 insertBatchSomeColumn 报方法找不到,如何解决? | 需让 Mapper 继承扩展包 BaseMapper ,或配置 SQL 注入器。 | 1. 方案 1(推荐): - Mapper 继承 com.baomidou.mybatisplus.extension.base.BaseMapper (扩展包含该方法);2. 方案 2(兼容旧 Mapper): - 配置 DefaultSqlInjector ,添加 InsertBatchSomeColumn 注入器;3. 版本要求:MyBatis-Plus ≥ 3.3.0。 |
MyBatis-Plus 字段查询 | getOne 后实体类是否只有 select 对应的字段有值? | 是,MyBatis 仅映射 SQL 查询返回的字段,未查询字段为 null (或默认值)。 | 1. 原理:SQL 若为 SELECT username FROM user ,则实体类仅 username 有值,id /age 等为 null ;2. 企业实践: - 必用 select() 指定字段,避免 SELECT * 浪费 IO;- 注意基本类型默认值(如 int 为 0),需做非空判断。 |
10、MyBatis-Plus updateById
(之类的同名函数)被自定义 XML 覆盖问题(含注释无效场景)整理表
核心维度 | 关键内容 |
---|---|
核心矛盾 | 自定义 XML 中 <update id="updateById"> 会覆盖 MP 默认实现,且仅注释标签无效(标签 ID 仍被识别),必须删除标签才生效 |
关键原因 | 1. 标签 ID 冲突:MP 默认updateById 与自定义标签 ID 重复,优先用 XML 配置2. 注释不彻底:XML 不识别 // //* */ ,部分版本即使<!-- --> 注释标签仍匹配 ID3. 缓存残留:删标签后未清 IDE / 服务器缓存,旧配置生效 |
唯一解决方案 | 1. 无特殊需求:彻底删除 XML 中所有<update id="updateById"> 标签(含头、尾、内容)2. 有特殊需求:自定义方法改名(如 updateSetmealById ),避免与 MP 默认方法名重名 |
避坑要点 | 1. MP 核心方法(insert /selectById /deleteById 等)无需写 XML,默认已实现2. 自定义方法必加业务前缀(如 updateSetmealXXX ),杜绝与默认方法名重复 |
11、链式调用 setStatus
报错问题分析与解决方案整理表
问题类别 | 具体现象 | 根本原因 | 解决方案(2 种核心方案) | 注意事项 |
---|---|---|---|---|
链式构造 set 方法报错 | 1. 调用 new OrderPO().setId(id).setStatus(...) 时报错2. 提示 “Cannot resolve method'setStatus (int)'” 3. 手动写 setStatus 方法仍无法链式调用 | 1. 仅用 @Data 注解,生成的 set 方法返回 void ,不支持链式调用(void 类型无法后续调用方法)2. 缺少支持链式构造的 Lombok 注解( @Accessors(chain = true) 或 @Builder ) | 方案 1:@Accessors(chain = true) 开启链式 set 1. 实体类添加注解: @Data + @Accessors(chain = true) 2. 调用语法不变: new OrderPO().setId(id).setStatus(常量) 方案 2: @Builder 生成建造者模式1. 实体类添加注解: @Data + @Builder 2. 调用语法改为建造者: OrderPO.builder().id(id).status(常量).build() | 1. @Accessors(chain = true) 需导入 lombok.experimental.Accessors 包2. @Builder 若字段有默认值,需配合 @Builder.Default 注解(如 @Builder.Default private Integer status = 0 ) |
普通 set 方法与链式调用混淆 | 误以为手动写 setStatus 方法即可链式调用,实际仍报错 | 手动写的普通 set 方法默认返回 void (如 public void setStatus(...) ),不符合链式调用 “返回当前对象(this )” 的要求 | 1. 若不用 Lombok,需手动写返回 this 的 set 方法:public OrderPO setStatus(Integer status) { this.status = status; return this; } 2. 推荐用 Lombok 注解( @Accessors /@Builder ),避免手动编写重复代码 | 手动写链式 set 方法时,返回值类型必须是当前实体类(OrderPO ),且每个 set 方法都需 return this |
Lombok 注解组合错误 | 仅加 @Builder 未加 @Data ,导致 getStatus 等方法缺失 | @Builder 仅生成建造者相关方法,不生成 get /set /toString 等基础方法;@Data 才会生成完整基础方法 | 实体类注解必须组合使用: - 方案 1 组合: @Data + @Accessors(chain = true) - 方案 2 组合: @Data + @Builder | 不可单独使用 @Accessors 或 @Builder ,需与 @Data 搭配(或搭配 @Getter +@Setter ) |
12、MyBatis 热销 Top10 统计问题 - 解决方案对照表
问题分类 | 具体问题描述 | 对应知识点 / 解决方案 | 关键代码 / 操作示例 |
---|---|---|---|
原代码性能问题 | 1. 循环查订单详情导致 N+1 查询;2. 重复查菜品 / 套餐名;3. 内存全量排序负载高 | 1. 用 SQL 联表查询替代循环查库;2. 数据库分组统计(SUM)替代内存计算;3. SQL 排序 + LIMIT 替代内存排序 | 联表 SQL:INNER JOIN order_detail od ON o.id = od.order_id ;统计:SUM(od.number) AS total_sales ;排序:ORDER BY total_sales DESC LIMIT 10 |
特殊场景替代方案 | 不想创建 DTO 时,如何接收 SQL 查询结果 | 可将resultType 设为java.util.Map ,MyBatis 自动映射 “列别名 - 字段值”,但需注意类型安全 | Mapper 接口返回值:List<Map<String, Object>> ;取值:map.get("product_name") 、map.get("total_sales") |
大数据量优化 | 订单量极大(日 10 万 +)时,联表查询效率低 | 1. 给关联字段加联合索引;2. 分表查询(如按时间分表order_202509 );3. Redis 缓存热点结果 | 索引示例:order(order_time, status) 、order_detail(order_id, dish_id, number) ;缓存:用 Redis 存储 Top10 结果,过期时间 1 小时 |
SQL 逻辑问题 | 订单详情中菜品 ID / 套餐 ID 互斥,如何统一获取名称;如何排除无效数据 | 1. 用COALESCE(d.name, s.name) 取非 null 名称;2. 加条件AND (od.dish_id IS NOT NULL OR od.setmeal_id IS NOT NULL) | SQL:COALESCE(d.name, s.name) AS product_name ;WHERE 条件:AND (od.dish_id IS NOT NULL OR od.setmeal_id IS NOT NULL) |
13、redisTemplate的引入方式:
对比维度 | 直接使用 Spring 自动配置 | 自己配置 RedisTemplate |
---|---|---|
配置工作量 | 零配置(直接注入) | 需写配置类,指定序列化器等 |
序列化机制 | 默认 JDK(乱码)/String 序列化 | 可自定义 JSON、ProtoBuf 等序列化器 |
Value 类型支持 | JDK 序列化:需 Serializable 类;StringRedisTemplate:仅 String | 任意类(取决于序列化器,如 JSON 支持所有类) |
灵活性 | 低(仅能通过 yml 简单配置) | 高(可定制连接池、客户端、序列化器) |
适用场景 | 简单 String 缓存(用 StringRedisTemplate) | 复杂对象存储、需可读格式、分布式场景 |
自己配置(会覆盖 Spring 自动配置的默认实例):
常用序列化器:
序列化器 | 支持的 Value 类型 | 特点 |
---|---|---|
StringRedisSerializer | 仅 String 类型 | 无乱码,适合纯字符串场景(如缓存 Token) |
JdkSerializationRedisSerializer | 实现 Serializable 接口的任意类 | 需类加 implements Serializable ,存储字节流(乱码) |
GenericJackson2JsonRedisSerializer | 任意类(无需实现接口) | 转 JSON 字节流,可读,支持复杂对象(推荐) |
Jackson2JsonRedisSerializer | 指定的单个类(如 User.class) | 需提前指定类,比 Generic 更高效,但灵活性低 |
企业级选型建议:
- 若仅存 String 类型(如 Token、简单缓存):直接用
StringRedisTemplate
(Spring 自动提供,无需自定义); - 若需存复杂对象(如 User、Order):必须自己配置
RedisTemplate
,指定 String 序列化(Key)+ JSON 序列化(Value)(推荐GenericJackson2JsonRedisSerializer
),避免乱码且支持任意类; - 若需极致性能(如高并发场景):可自定义
Jackson2JsonRedisSerializer
(指定具体类),比 Generic 更高效,但需为每个类单独配置。