苍穹外卖项目总结(一)[MyBatis-Plus,文件上传,Redis]

苍穹外卖项目复习笔记

一、 MyBatis-Plus (MP) 核心应用

这部分是开发数据持久层的核心,重点在于理解MP如何简化开发以及如何处理复杂场景。

1. 基础 CRUD 与架构关系

  • BaseMapper vs ServiceImpl :
    • BaseMapper<T>: 位于DAO层。提供了最底层的数据库原子操作(insert, deleteById, update, selectList等)。直接操作数据库。
    • ServiceImpl<M, T> : 位于Service层。实现了IService<T>接口,内部默认注入了BaseMapper。它在BaseMapper的基础上进行了封装,提供了更高级的业务逻辑支持(如批量操作、链式调用)。
    • 总结: 简单SQL直接调Mapper,复杂业务逻辑或批量操作调Service。

2. 分页查询

  • 单表分页 :
    • 配置 : 必须配置MybatisPlusInterceptor并添加PaginationInnerInterceptor拦截器,否则分页无效(只会查全部)。
    • 实现 : 使用Page<T>对象作为参数传入BaseMapper的查询方法中。
  • 多表关联分页查询 :
    • 难点: MP的Wrapper主要针对单表。
    • 解决: 手写SQL(XML或注解)。
    • 关键 : 只要Service方法定义的第一个参数是Page对象,MP拦截器会自动拦截该SQL,执行SELECT count(0)查询总数,然后注入LIMIT语句,无需手动写分页逻辑。

具体实现

自定义 SQL (XML 方式) ------ 推荐

核心思路:编写一个 SQL,使用 LEFT JOIN 连接菜品表和分类表,直接查出你需要的所有字段,让 MP 帮你做分页。

  1. 定义 DishVO, 确保你的 DishVO 里有 categoryName 字段。
java 复制代码
@Data
public class DishVO extends Dish {
    // 继承了 Dish 的所有属性
    
    // 额外添加分类名称
    private String categoryName; 
    
    // 可能还需要口味列表,根据你的业务需求决定是否在这里查,或者分开查
    // private List<DishFlavor> flavors; 
}
  1. 修改 Mapper 接口
    DishMapper.java 中定义一个方法。 注意:
  • 入参必须包含 IPage,MP 会自动识别它并进行分页拦截。
  • 返回值也是 IPage。
java 复制代码
@Mapper
public interface DishMapper extends BaseMapper<Dish> {

    /**
     * 自定义分页查询 DishVO
     * @param page MP 分页对象,必须作为第一个参数
     * @param dishPageQueryDTO 查询条件
     * @return 分页结果
     */
    Page<DishVO> pageQuery(IPage<DishVO> page, @Param("dto") DishPageQueryDTO dishPageQueryDTO);
}
  1. 编写 Mapper XML,在 DishMapper.xml 中编写 SQL。
XML 复制代码
<mapper namespace="com.sky.mapper.DishMapper">

    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        SELECT 
            d.*, 
            c.name AS category_name 
        FROM dish d
        LEFT JOIN category c ON d.category_id = c.id
        <where>
            <if test="dto.name != null and dto.name != ''">
                AND d.name LIKE CONCAT('%', #{dto.name}, '%')
            </if>
            <if test="dto.categoryId != null">
                AND d.category_id = #{dto.categoryId}
            </if>
            <if test="dto.status != null">
                AND d.status = #{dto.status}
            </if>
        </where>
        ORDER BY d.create_time DESC
    </select>

</mapper>
  1. Service 层调用
    直接调用 Mapper 即可,非常简洁。
Java 复制代码
@Override
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
    // 1. 准备分页对象
    Page<DishVO> page = new Page<>(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
    
    // 2. 调用 Mapper 自定义方法
    // MP 会自动执行两条 SQL:
    // 第一条:SELECT COUNT(*) ... LEFT JOIN ...
    // 第二条:SELECT d.*, c.name ... LEFT JOIN ... LIMIT ?, ?
    Page<DishVO> voPage = dishMapper.pageQuery(page, dishPageQueryDTO);

    // 3. 封装结果
    return new PageResult(voPage.getTotal(), voPage.getRecords());
}

3. 批量操作与链式调用 (Point 6)

  • 批量操作 : 使用IService接口提供的方法,如saveBatch(List<T>)updateBatchById(List<T>)。底层是基于JDBC的batch操作,性能优于循环调用Mapper。
  • 链式调用 (LambdaQueryChainWrapper) :
    • 场景: 避免创建繁琐的Wrapper对象。
    • 写法 : lambdaQuery().eq(User::getId, id).one();
    • 优点: 代码更优雅,可读性更强。

4. 公共字段填充 (Point 4)

  • MP 原生方式 (MetaObjectHandler) :
    • 原理 : 实现MetaObjectHandler接口,重写insertFillupdateFill方法。
    • 配置 : 在实体类字段上添加@TableField(fill = FieldFill.INSERT)等注解。
    • 场景: 适用于纯数据库层面的统一赋值(如create_time, update_time)。
为什么推荐 MetaObjectHandler?
  • 更底层: 它直接介入 MyBatis-Plus 的 CRUD 操作,是专门为数据填充设计的。

  • 更简单: 避免了复杂的 AOP 反射和切入点配置。

  • 更清晰: 逻辑集中在一个处理器类中,明确区分了 插入更新 两种场景。

实现步骤
步骤 1:创建元对象处理器

创建一个类继承自 com.baomidou.mybatisplus.core.handlers.MetaObjectHandler,并实现 insertFillupdateFill 方法。

java 复制代码
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    // 假设您有一个工具类来获取当前用户ID
    private Long getCurrentUserId() {
        // 实际应用中:从 ThreadLocal 或 SecurityContext 获取当前登录用户ID
        return 1L; // 示例值
    }

    /**
     * 插入操作时自动填充
     */
    @Override
    public void insertFill(MetaObject metaObject) {
        // 1. 填充创建时间
        if (metaObject.hasSetter("createTime")) {
            this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        }
        
        // 2. 填充创建人
        if (metaObject.hasSetter("createUser")) {
            this.strictInsertFill(metaObject, "createUser", Long.class, getCurrentUserId());
        }
        
        // 3. 插入时通常也填充更新时间和更新人
        if (metaObject.hasSetter("updateTime")) {
            this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        }
        if (metaObject.hasSetter("updateUser")) {
            this.strictInsertFill(metaObject, "updateUser", Long.class, getCurrentUserId());
        }
    }

    /**
     * 更新操作时自动填充
     */
    @Override
    public void updateFill(MetaObject metaObject) {
        // 1. 填充更新时间
        if (metaObject.hasSetter("updateTime")) {
            this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        }
        
        // 2. 填充更新人
        if (metaObject.hasSetter("updateUser")) {
            this.strictUpdateFill(metaObject, "updateUser", Long.class, getCurrentUserId());
        }
    }
}
步骤2:在实体类上添加注解

在您的实体类(如 Employee)中,只需在需要自动填充的字段上添加 @TableField 注解,指定填充策略。

java 复制代码
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import java.time.LocalDateTime;

public class Employee {
    // ... 其他字段

    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    @TableField(fill = FieldFill.INSERT)
    private Long createUser;

    // 插入和更新时都需要填充
    @TableField(fill = FieldFill.INSERT_UPDATE) 
    private LocalDateTime updateTime;

    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
}
  • AOP 方式 :
    • 原理: 自定义注解 + Aspect切面。在切面中通过反射给参数对象的属性赋值。
    • 场景 : 适用于需要获取上下文信息(如从ThreadLocal获取当前登录用户的ID)并填充到实体的场景。苍穹外卖中常结合两者使用。
苍穹外卖中AOP机制的分析

切面配置使用了 前置通知 和一个非常精确的 切入点表达式。

A. 切面类:AutoFillAspect

  • 在项目中,AutoFillAspect 是一个使用 @Aspect 注解的 Spring Bean。它很可能使用了 前置通知 (@Before) 来实现公共字段填充。
  • 执行逻辑: 在 Mapper 方法执行之前,通过反射获取方法参数(通常是实体对象),然后根据当前操作类型(INSERT 或 UPDATE)和当前登录的用户信息,为实体对象的 createTime、createUser、updateTime、updateUser 字段赋值。

B. 切入点表达式解析

切入点表达式是:
com.sky.mapper.*.*(..) && @annotation(com.sky.annotation.autoFill)\text{com.sky.mapper.*.*(..) \&\& @annotation(com.sky.annotation.autoFill)}com.sky.mapper.*.*(..) && @annotation(com.sky.annotation.autoFill)方法匹配部分:com.sky.mapper.*.*(..)com.sky.mapper.*: 匹配 com.sky.mapper 包下的所有类 (通常是 Mapper 接口)。.*(...): 匹配这些类中的所有方法 (方法名和参数数量不限)。

效果: 限制了切面只在 mapper 包下的方法上查找连接点。

注解匹配部分:@annotation(com.sky.annotation.autoFill)@annotation(...): 匹配所有被括号内指定的注解标注的方法。

效果: 只有当 com.sky.mapper 包下的某个方法同时被 @AutoFill 注解标记时,切面才会被织入。

总结: 只有当您在 Mapper 接口中定义的方法(如 insert 或 update 方法)上同时使用了 @AutoFill 注解时,AutoFillAspect 的逻辑才会在该方法执行时被触发。


二、 架构设计与难点解决

这部分涉及企业级开发的规范和常见"坑"的解决方案。

1. 雪花算法精度丢失问题

  • 现象 : 后端ID为Long类型(19位),前端JS的Number类型最大安全整数是253−12^{53}-1253−1(约16位)。传递到前端时,后几位会被四舍五入,导致ID不一致。
  • 解决方案 : 后端将Long转为String传输
  • 实现方式 :
    • 局部处理 : 在实体类ID字段上加注解 @JsonSerialize(using = ToStringSerializer.class)
    • 全局处理 : 配置Jackson的消息转换器 (MappingJackson2HttpMessageConverter),将所有Long类型序列化为String。

2. 企业级文件上传设计

  • 技术栈: 阿里云 OSS (Object Storage Service)。
  • 设计模式 :
    • XXXProperties : 使用@ConfigurationProperties读取application.yml中的配置(endpoint, key, secret, bucket),实现配置与代码分离。
    • XXXConfiguration : 使用@Configuration@Bean,利用Properties创建OSS客户端对象,交给Spring容器管理。
    • 接口设计 : 定义标准的FileStorageService接口,包含upload方法。
    • 多实现类: 如果未来切换到腾讯云COS或MinIO,只需新增实现类并修改配置,无需修改业务代码(符合开闭原则)。

三、 中间件集成 (Redis)

StringRedisTemplate vs RedisTemplate

特性 RedisTemplate StringRedisTemplate
序列化方式 默认使用 JdkSerializationRedisSerializer 默认使用 StringRedisSerializer
存储格式 二进制流(乱码状,不可读) 纯字符串(清晰可读)
占用空间 较大(包含类信息) 较小
适用场景 存储复杂的Java对象,且不关心Redis中数据的可读性 存储简单的KV结构,或者需要与其他语言交互,或者需要人工排查Redis数据
推荐 一般不推荐直接用默认的。通常会自定义配置JSON序列化器。 推荐使用。手动将对象转为JSON字符串存入,取出来再转回对象。
相关推荐
编程饭碗17 分钟前
【Spring全局异常处理 早抛晚捕】
java·数据库·spring
langsiming43 分钟前
Redis底层实现
数据库·redis·缓存
Hello World呀1 小时前
Redis是AP的还是CP?
数据库·redis·缓存
皇族崛起2 小时前
【视觉多模态】- 3D建模尝试 I (广场3D建模,失败)
数据库·人工智能·3d·性能优化
JavaLearnerZGQ2 小时前
redis笔记大全
数据库·redis·笔记
资生算法程序员_畅想家_剑魔3 小时前
Java常见技术分享-26-事务安全-锁机制-作用与分类
java·开发语言·数据库
Vic101013 小时前
PostgreSQL 中 nextval() 的线程安全性解析
java·数据库·postgresql
写代码的小阿帆3 小时前
Redis缓存健壮性——穿透、雪崩与击穿防护
数据库·redis·缓存
wangqiaowq4 小时前
使用 mysqldump 导出 + mysql 导入
数据库
qq_317620314 小时前
第23章-中级项目练习案例(15个)
数据库·爬虫·web开发·python项目·api开发·python案例