MyBatis-Plus 深度开发规范手册

821 阅读7分钟

一、 核心架构分层规范 (数据流转)

  1. 强制: 遵循 Controller -> Service -> Mapper 的单向调用链。
  2. 强制: 严禁 Service 循环依赖。若 A 需调用 B,B 也需调用 A,必须将共有逻辑提取至第三个 Service 或 Manager 层。
  3. 规范: 所有的 POJO 转换(Entity ↔ DTO ↔ VO)建议使用 MapStruct,避免手动 BeanUtils.copyProperties 导致的反射性能损耗及字段名对不上的 Bug。

二、 实体类 (Entity) 与数据库建模

  1. 强制: 核心主键使用 Long 类型,并配合 Jackson 解决 JS 精度丢失问题:

    @TableId(type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class) // 防止前端丢失最后两位精度
    private Long id;
    
  2. 规范: 如有需要使用 乐观锁 机制防止数据并发覆盖。

    • 数据库增加 version 字段。
    • Entity 增加 @Version 注解。
    @Data
    public class User extends BaseEntity {
        // ... 其他字段
    
        @Version // 关键:标记为乐观锁字段
        private Integer version;
    }
    
    // 注入插件(在 Config 类中)
    interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
    
    • 只支持 updateByIdupdate 方法:乐观锁插件只有在通过实体对象(带 ID 和 Version)进行更新时才生效。

    • 处理失败逻辑:当 update 返回 0(失败)时,你需要决定是报错给前端“系统繁忙”,还是自动发起重试(重新查询 -> 修改 -> 再提交)。

    • 严禁手动改 Version:在业务代码里,你不需要执行 user.setVersion(user.getVersion() + 1),MyBatis-Plus 插件会自动帮你完成这个动作。

  3. 规范: 状态字段建议使用 IEnum 接口枚举,避免代码中出现 if (status == 1)

    public enum StatusEnum {
        DISABLED(0, "禁用"), ACTIVE(1, "启用");
    
        @EnumValue // 标记这个字段存入数据库
        private final int code;
    
        private final String desc;
    }
    

三、 事务管理规约 (防止 10% 的隐形 Bug)

  1. 强制: 严禁在 Service 内部 自调用 标注了 @Transactional 的方法(会导致 AOP 事务失效)。

    • 修正方案: 拆分到不同类,或使用 AopContext.currentProxy()。或自己注入自己
    // 使用延迟加载,避免启动时的循环依赖检测问题 
    @Autowired 
    @Lazy 
    private UserService self;
    
  2. 强制: 事务方法内严禁进行 网络 IO 操作(如调用第三方支付接口、发邮件)。

    • 原因: 网络延迟会导致数据库连接长期不释放,瞬间撑爆连接池。
  3. 规范: 声明式事务建议只加在 update/insert 方法上,查询方法不加事务。


四、 代码编写“六不准” (硬性军规)

  1. 不准在 IN 查询中传入超过 1000 个元素

    • 风险: 部分数据库(如 Oracle)有 1000 个上限,且 SQL 过长导致索引失效。
  2. 不准在 Mapper.xml 中使用 ${}

    • 风险: 99% 的 SQL 注入源于此。除 ORDER BY 动态排序外,一律使用 #{}
  3. 不准在没有 Where 条件下执行 update/delete

    • 措施: 必须配置 BlockAttackInnerInterceptor 插件。
  4. 不准在循环中调用 saveOrUpdate

    • 风险: 每次判断存在都会先执行一次 SELECT,导致数据库压力翻倍。
  5. 不准省略逻辑删除过滤

    • 注意: 手写 SQL 时,MP 不会自动拼接 deleted=0,必须手动在 XML 中加上。
  6. 不准使用“魔法值”:Lambda 表达式

    • 禁止: 使用字符串硬编码列名(如 queryWrapper.eq("user_name", "Jack"))。

    • 强制: 必须使用 Lambda 形式,确保编译期类型检查。

    // 正确示例
    this.lambdaQuery()
        .eq(User::getUserName, dto.getUserName())
        .orderByDesc(User::getCreateTime)
        .list();
    

五、 SQL 与性能优化规范 (解决 5% 的慢查询 Bug)

  1. 强制: 凡是 like 查询,严禁使用左模糊(%关键词),必须使用右模糊关键词%),否则索引失效。

    • likeRight("name", "张")
    • like("name", "张")
  2. 推荐: 针对大数据量表,禁止执行 select count(*),应通过业务逻辑或 Redis 维护总数。

  3. 规范: 尽量避免多表 Join。阿里规约建议:超过 3 张表禁止 Join。

    • 对策: 分两次查询,在 Service 层使用 Map 组装数据。

六、 审计与扩展功能

1. 自动填充 (MetaObjectHandler)

强制: 严禁手动维护 update_time 等公共字段。

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    }
}

2. 字段脱敏

规范: 手机号、身份证、密码等敏感信息,在 Entity 或 VO 上配合 @TableField(select = false) 或自定义 Jackson 序列化器进行脱敏。


七、 投产前 Check List (最后 5% 的 Bug 拦截)

-  **空指针检查**`LambdaQuery` 传入参数前是否判断了 `null`?(或使用 `.condition()`-  **分页溢出**:是否配置了 `PaginationInnerInterceptor` 且设置了最大单页限制(如 500 条)?
-  **字段对应**:Entity 里的字段在数据库中是否都存在?(不存在的需加 `@TableField(exist = false)`-  **日志检查**`application-prod.yml` 是否关闭了 SQL 日志打印?(防止日志文件撑爆磁盘)
-  **缓存同步**:如果使用了 Redis 缓存,`updateById` 后是否同步删除了缓存?

八 全量配置规范实现

1. 核心插件配置类 (MybatisPlusConfig.java)

本配置类集成了常用的插件,确保系统在大数据量下的安全与性能。

package com.yourcompany.project.common.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MyBatis-Plus 全量标准化配置
 * 包含:分页、乐观锁、防全表攻击插件
 * * @author 专业Java程序员
 */
@Configuration
@MapperScan("com.yourcompany.project.mapper") // 修改为你的 Mapper 实际路径
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();

        // 1. 分页插件: 必须设置数据库类型,且建议放在插件链的最前面
        // 自动防止分页超出总页数(溢出处理)
        PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInterceptor.setMaxLimit(500L); // 限制单页最大 500 条,防止内存溢出
        interceptor.addInnerInterceptor(paginationInterceptor);

        // 2. 乐观锁插件: 实体类配合 @Version 注解使用,防止并发冲突
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        // 3. 防全表更新与删除插件: 禁止没有 where 条件的 update/delete,规避重大事故
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

        return interceptor;
    }
}

2. 审计字段自动填充 (MyMetaObjectHandler.java)

实现阿里巴巴规约中“公共字段统一维护”的要求,解决手动设置 create_time 等产生的遗漏 Bug。

package com.yourcompany.project.common.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

/**
 * 自动化审计字段填充器
 */
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {
        log.info("start insert fill ....");
        // 填充创建时间与更新时间
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
        // 填充默认删除位(0: 未删除)
        this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
        // 填充版本号(乐观锁初始值)
        this.strictInsertFill(metaObject, "version", Integer.class, 1);
        
        // 如果有安全框架(如Spring Security),可动态填充当前登录用户ID
        // Long userId = SecurityUtils.getUserId();
        // this.strictInsertFill(metaObject, "createBy", Long.class, userId);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        log.info("start update fill ....");
        // 更新操作时,自动更新修改时间
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

3. 标准化基类实体 (BaseEntity.java)

封装 100% 实体类都需要具备的字段,减少代码冗余。

package com.yourcompany.project.common.entity;

import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import java.time.LocalDateTime;

@Data
public abstract class BaseEntity {

    /**
     * 主键: 雪花算法,解决分布式ID唯一性
     * ToStringSerializer: 解决前端 JS 无法处理 19 位 Long 导致的精度丢失问题
     */
    @TableId(type = IdType.ASSIGN_ID)
    @JsonSerialize(using = ToStringSerializer.class)
    private Long id;

    /**
     * 自动填充创建时间
     */
    @TableField(fill = FieldFill.INSERT)
    private LocalDateTime createTime;

    /**
     * 自动填充更新时间
     */
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private LocalDateTime updateTime;

    /**
     * 乐观锁版本号: 配合 OptimisticLockerInnerInterceptor 使用
     */
    @Version
    @TableField(fill = FieldFill.INSERT)
    private Integer version;

    /**
     * 逻辑删除位: 配合 @TableLogic 使用
     */
    @TableLogic
    @TableField(fill = FieldFill.INSERT)
    private Integer deleted;
}

4. 重点应用说明

功能解决的潜在 Bug
分页 MaxLimit防止恶意请求 size=999999 撑爆 JVM 内存。
BlockAttack防止因代码 Bug 或运维误操作导致 DELETE FROM table(无条件全表清空)。
ToStringSerializer解决雪花 ID 最后两位在浏览器里变 00 的经典前端精度 Bug。
OptimisticLocker解决高并发下“先读后写”导致的数据相互覆盖(Lost Update)。
StrictInsertFill保证所有数据均有完整的审计追踪(谁在什么时候创建的)。

使用建议

  1. 全局扫描:请确保 MybatisPlusConfig 中的 @MapperScan 路径与你实际的 Mapper 接口位置一致。

  2. 配置文件同步:在 application.yml 中开启逻辑删除全局配置(logic-delete-field: deleted)。

  3. YAML 配合

    YAML

    mybatis-plus:
      global-config:
        db-config:
          logic-delete-field: deleted # 全局逻辑删除字段
          logic-delete-value: 1
          logic-not-delete-value: 0
    

总结

规范不是为了限制开发速度,而是为了在凌晨 3 点不需要爬起来处理生产事故。