Skip to content
第 53 / 250 章后端⏱ 10 分钟阅读

第 53 章:MyBatis-Plus 入门

学习目标

  • 完成 MyBatis-Plus 集成
  • 掌握 BaseMapper 与 IService 通用方法
  • 理解常用注解与逻辑删除

一、为什么用 MyBatis-Plus?

维度原生 MyBatisMyBatis-Plus
单表 CRUD每张表手写 XML内置,零 SQL
分页手写 limit + count插件自动处理
条件构造拼 XML 的 <if> 标签Lambda 链式 API
主键生成自己处理内置雪花算法
逻辑删除每条 SQL 加 deleted=0注解一行搞定
自动填充手动 set注解自动
侵入性-零侵入,原生 MyBatis 功能全保留

核心价值:把 80% 的重复 CRUD 代码消灭掉,让你专注复杂业务 SQL。

二、集成

依赖

xml
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>  <!-- ① Boot 3 专用 -->
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
</dependency>

⚠️ Spring Boot 3 必须用 mybatis-plus-spring-boot3-starter。用了老的 mybatis-plus-boot-starter 会因为 javaxjakarta 包名变更而启动失败。

配置

yaml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/taskflow?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
    username: root
    password: ${DB_PASSWORD}

mybatis-plus:
  mapper-locations: classpath*:/mapper/**/*.xml    # ① XML 位置
  type-aliases-package: com.taskflow.**.entity     # ② 实体别名包
  configuration:
    map-underscore-to-camel-case: true             # ③ user_name → userName
    cache-enabled: false                           # ④ 关闭二级缓存(用 Redis 更可控)
    # 开发环境打印 SQL,生产环境删掉这行
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    banner: false
    db-config:
      id-type: assign_id                           # ⑤ 默认雪花 ID
      logic-delete-field: deleted                  # ⑥ 全局逻辑删除字段
      logic-delete-value: 1
      logic-not-delete-value: 0
      update-strategy: not_null                    # ⑦ 更新时忽略 null 字段

连接串参数 rewriteBatchedStatements=true 很重要:不加的话 MySQL 驱动的批量插入其实是一条条发的,加了之后会合并成一条大 SQL,批量插入性能提升十倍以上

启动类扫描

java
@SpringBootApplication
@MapperScan("com.taskflow.**.mapper")     // ① 扫描 Mapper 接口
public class TaskflowApplication { }

三、实体类注解

java
@Data
@TableName("sys_user")                            // ① 表名
public class User {

    @TableId(type = IdType.ASSIGN_ID)             // ② 主键:雪花算法
    private Long id;

    private String username;                       // ③ 无注解时按驼峰转下划线映射

    @TableField("nick_name")                       // ④ 字段名不符合驼峰规则时显式指定
    private String nickname;

    @TableField(select = false)                    // ⑤ 查询时不返回(如密码)
    private String password;

    @TableField(exist = false)                     // ⑥ 数据库中不存在的字段
    private String deptName;

    @TableField(fill = FieldFill.INSERT)           // ⑦ 插入时自动填充
    private LocalDateTime createTime;

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

    @TableLogic                                    // ⑨ 逻辑删除标记
    private Integer deleted;

    @Version                                       // ⑩ 乐观锁版本号
    private Integer version;
}

主键策略

策略说明适用
ASSIGN_ID雪花算法(默认)推荐,分布式友好
AUTO数据库自增单库单表
ASSIGN_UUIDUUID 字符串需要无序 ID
INPUT手动设置业务主键
NONE无策略特殊场景

为什么推荐雪花 ID 而不是自增?

  1. 分库分表时自增 ID 会冲突
  2. 自增 ID 暴露业务量(订单号 1001 → 竞争对手知道你只有 1000 单)
  3. 插入时不需要回表拿 ID

雪花 ID 的坑Long 类型是 19 位,超过 JavaScript 的 Number.MAX_SAFE_INTEGER(16 位),前端会丢精度。解决:序列化成字符串。

java
// 全局配置:Long 类型序列化为 String
@Configuration
public class JacksonConfig {
    @Bean
    public Jackson2ObjectMapperBuilderCustomizer customizer() {
        return builder -> builder
                .serializerByType(Long.class, ToStringSerializer.instance)
                .serializerByType(Long.TYPE, ToStringSerializer.instance);
    }
}

四、BaseMapper:开箱即用

java
public interface UserMapper extends BaseMapper<User> {
    // 空的!单表 CRUD 全都有了
}
java
// 增
userMapper.insert(user);

// 删
userMapper.deleteById(1L);
userMapper.deleteBatchIds(List.of(1L, 2L, 3L));
userMapper.delete(new LambdaQueryWrapper<User>().eq(User::getStatus, 0));

// 改
userMapper.updateById(user);                       // 按 ID 更新非 null 字段
userMapper.update(user, wrapper);                  // 按条件更新

// 查
userMapper.selectById(1L);
userMapper.selectBatchIds(List.of(1L, 2L));
userMapper.selectList(wrapper);
userMapper.selectOne(wrapper);                     // ⚠️ 结果多于 1 条会抛异常
userMapper.selectCount(wrapper);
userMapper.selectPage(page, wrapper);
userMapper.selectMaps(wrapper);                    // 返回 List<Map>

五、IService:Service 层增强

java
// ① 接口继承 IService
public interface UserService extends IService<User> {
    UserVO getVOById(Long id);                     // 自己的业务方法
}

// ② 实现类继承 ServiceImpl
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {

    @Override
    public UserVO getVOById(Long id) {
        User user = getById(id);                   // ③ 直接用父类方法
        return UserConvert.INSTANCE.toVO(user);
    }
}

IService 相比 BaseMapper 多了什么?

java
// ① 批量操作(内部分批,默认 1000 一批)
userService.saveBatch(users);
userService.saveBatch(users, 500);                 // 指定批次大小
userService.updateBatchById(users);
userService.saveOrUpdateBatch(users);

// ② 存在则更新,不存在则插入
userService.saveOrUpdate(user);

// ③ 链式调用(最常用)
userService.lambdaQuery()
        .eq(User::getStatus, 1)
        .like(User::getUsername, "张")
        .orderByDesc(User::getCreateTime)
        .list();

userService.lambdaUpdate()
        .eq(User::getId, 1L)
        .set(User::getStatus, 0)
        .update();

// ④ getOne 加 throwEx 参数,多条时不抛异常
userService.getOne(wrapper, false);                // 返回第一条

// ⑤ 链式查询的便捷终端方法
userService.lambdaQuery().eq(...).one();           // 单条
userService.lambdaQuery().eq(...).list();          // 列表
userService.lambdaQuery().eq(...).count();         // 计数
userService.lambdaQuery().eq(...).exists();        // 是否存在
userService.lambdaQuery().eq(...).page(page);      // 分页

saveBatch 的性能真相:MyBatis-Plus 的 saveBatch 是循环执行单条 insert,只是共用一个 session。真正的高性能批量插入需要 rewriteBatchedStatements=true(见上文)或自己写 INSERT INTO ... VALUES (),(),() 的 XML。

六、逻辑删除

java
@TableLogic
private Integer deleted;      // 0=未删除,1=已删除

效果:MyBatis-Plus 自动改写 SQL

sql
-- 你写的:userMapper.deleteById(1L)
UPDATE sys_user SET deleted = 1 WHERE id = 1 AND deleted = 0;

-- 你写的:userMapper.selectById(1L)
SELECT * FROM sys_user WHERE id = 1 AND deleted = 0;

-- 你写的:userMapper.selectList(null)
SELECT * FROM sys_user WHERE deleted = 0;

⚠️ 逻辑删除的三大坑

坑 1:唯一索引冲突

sql
-- 用户名唯一索引
UNIQUE KEY uk_username (username)

-- 张三注册 → 删除 → 再注册 → ❌ 唯一索引冲突(老记录还在,只是 deleted=1)

解决

sql
-- 方案 A:唯一索引包含删除标记(deleted 存删除时间戳而非 0/1)
UNIQUE KEY uk_username (username, deleted)
-- deleted 未删除时为 0,删除时设为当前时间戳 → 可重复删除同一用户名

-- 方案 B:不用逻辑删除,删除时把数据移到归档表

坑 2:自定义 SQL 不生效

xml
<!-- ❌ 手写的 SQL,MyBatis-Plus 管不着,会查出已删除数据 -->
<select id="selectCustom" resultType="User">
    SELECT * FROM sys_user WHERE dept_id = #{deptId}
</select>

<!-- ✅ 必须手动加条件 -->
<select id="selectCustom" resultType="User">
    SELECT * FROM sys_user WHERE dept_id = #{deptId} AND deleted = 0
</select>

坑 3:数据量膨胀

删除的数据永远留在表里,几年后表里 90% 是垃圾数据,查询越来越慢。

对策:定时归档任务,把 deleted=1 且超过 N 天的数据迁到归档表后物理删除。

七、乐观锁

java
@Version
private Integer version;
java
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

效果

java
User user = userMapper.selectById(1L);      // version = 1
user.setStatus(0);
int rows = userMapper.updateById(user);
// 实际 SQL: UPDATE sys_user SET status=0, version=2 WHERE id=1 AND version=1
// 如果期间被别人改过(version 已是 2),rows = 0,更新失败
java
// 必须判断返回值!
if (userMapper.updateById(user) == 0) {
    throw new BusinessException(ErrorCode.CONCURRENT_UPDATE, "数据已被他人修改,请刷新重试");
}

⚠️ 乐观锁只在 updateByIdupdate(entity, wrapper) 时生效lambdaUpdate().set() 这种不带实体的更新不会走乐观锁。

八、必备插件配置

java
@Configuration
@MapperScan("com.taskflow.**.mapper")
public class MybatisPlusConfig {

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

        // ① 分页插件(必须第一个加,顺序有讲究)
        PaginationInnerInterceptor page = new PaginationInnerInterceptor(DbType.MYSQL);
        page.setMaxLimit(500L);                      // 单页上限,防止恶意大分页
        page.setOverflow(false);                     // 页码超出最大页时不回到首页
        interceptor.addInnerInterceptor(page);

        // ② 乐观锁
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

        // ③ 防全表更新删除(安全兜底)
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());

        return interceptor;
    }
}

BlockAttackInnerInterceptor 强烈建议开启。它会阻止没有 WHERE 条件的 UPDATE / DELETE。真实事故:某开发写了 userMapper.delete(new QueryWrapper<>())(wrapper 为空),把整张表删了。

九、本章小结

要点关键
Boot 3mybatis-plus-spring-boot3-starter
连接串rewriteBatchedStatements=true
雪花 IDLong 要序列化成 String,防前端精度丢失
BaseMapper单表 CRUD 全包
IService批量、链式、saveOrUpdate
逻辑删除注意唯一索引冲突、自定义 SQL 不生效
乐观锁必须判断返回行数
插件分页 + 乐观锁 + 防全表操作

动手练习

练习 1:基础题

创建 sys_user 表,配置 MyBatis-Plus,用 BaseMapper 完成增删改查,验证逻辑删除生效(数据没真删)。

练习 2:进阶题

模拟并发场景:两个线程同时读取同一条记录并修改,验证乐观锁能拦住其中一个。


下一章第 54 章:条件构造器与分页

本站基于 VitePress 构建 · 由 Codebook 团队维护