MyBatis-Plus

发布于 2026-07-30 19:29 更新于 2026-07-30 19:29 3973 字 20 min read ... 访问量

本文介绍了 MyBatis-Plus 的核心特性与使用场景,强调其在原生 MyBatis 基础上通过无侵入方式增强,实现通用 CRUD、条件构造器、分页、逻辑删除、乐观锁等高级功能,大幅提升开发效率。重点讲解了实体类注解、通用 CRUD 操作、Wrapper 条件构造器、自动填充、逻辑删除及常见问题排查,并深入剖析了其底层执行原理与与 MyBatis 的关系,适合已掌握 Spring Boot 和 MyBatis 的开发者快速上手并应对面试。

MyBatis-Plus

课程前置说明

适用人群:已掌握 MyBatis 基础和 Spring Boot 基础的开发者或学生

课程示例环境:Spring Boot 3.2.x、MyBatis-Plus 3.5.6、MySQL 8.x

课程目标

  • 掌握 MyBatis-Plus 核心特性、自动 CRUD、条件构造器
  • 精通分页、排序、逻辑删除、乐观锁等高级功能
  • 理解 MyBatis-Plus 底层执行原理、自动注入机制
  • 搞定高频 MyBatis-Plus 面试原理题

核心优势(对比原生 MyBatis)

原生 MyBatis 需要手动编写大量 XML/注解 SQL 实现增删改查,MyBatis-Plus 基于 MyBatis 增强,无侵入、只增强,封装通用 CRUD,告别重复 SQL,大幅提升开发效率。

MyBatis-Plus 快速入门

MyBatis-Plus 核心简介

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

核心特性

  • 无侵入:仅增强,不修改原生 MyBatis 代码,兼容原有 MyBatis 项目
  • 低损耗:启动自动注入通用 CRUD,性能几乎无损耗
  • 强大 CRUD:内置通用 Mapper、通用 Service,单表操作零 SQL
  • 条件构造器:Wrapper 动态拼接 SQL,告别硬编码 SQL
  • 高级功能:分页、逻辑删除、乐观锁、主键自动生成、多租户等

环境搭建(Spring Boot 与 MP)

引入核心依赖

pom.xml 核心依赖(Spring Boot 父工程已配置)

<!-- MyBatis-Plus 的 Spring Boot 3 Starter -->
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
   <version>3.5.6</version>
</dependency>

<!-- MySQL 驱动 -->
<dependency>
   <groupId>com.mysql</groupId>
   <artifactId>mysql-connector-j</artifactId>
   <scope>runtime</scope>
</dependency>

<!-- Lombok,可选 -->
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <optional>true</optional>
</dependency>

<!-- 测试依赖 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-test</artifactId>
   <scope>test</scope>
</dependency>

全局配置(application.yml)

spring:
 # 数据源配置
 datasource:
   url: jdbc:mysql://localhost:3306/java2601?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
   username: root
   password: root
   driver-class-name: com.mysql.cj.jdbc.Driver
# MyBatis-Plus 全局配置
mybatis-plus:
 # 映射文件路径
 mapper-locations: classpath:mapper/*.xml
 # 实体类别名包
 type-aliases-package: com.mp.demo.entity
 configuration:
   # 开启下划线转驼峰自动映射
   map-underscore-to-camel-case: true
   # 开发环境打印 SQL,生产环境通常关闭
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 global-config:
   db-config:
     # 主键自增策略
     id-type: auto
     # 数据库表前缀(可选)
     # table-prefix: tb_
     # 逻辑删除全局配置
     logic-delete-field: deleteFlag
     logic-delete-value: 1 # 已删除
     logic-not-delete-value: 0 # 未删除

启动类注解

推荐使用 @MapperScan 统一扫描 Mapper 接口。也可以在每个接口上添加 @Mapper,两种方式选择一种即可。

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.Spring BootApplication;
@Spring BootApplication
@MapperScan("com.mp.demo.mapper")
public class MpDemoApplication {
   public static void main(String[] args) {
       SpringApplication.run(MpDemoApplication.class, args);
   }
}

基础工程结构

com.mp.demo
├── entity      // 数据库实体类
├── mapper      // Mapper 接口(继承BaseMapper)
├── service     // 业务层
│   └── impl    // 业务实现类(继承ServiceImpl)
└── controller  // 控制层

核心基础:实体类注解与主键策略

数据库表准备

CREATE TABLE `user` (
 `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
 `name` varchar(30) DEFAULT NULL COMMENT '姓名',
 `age` int DEFAULT NULL COMMENT '年龄',
 `email` varchar(50) DEFAULT NULL COMMENT '邮箱',
 `delete_flag` tinyint(1) DEFAULT 0 COMMENT '逻辑删除标识 0-未删除 1-已删除',
 `create_time` datetime DEFAULT NULL COMMENT '创建时间',
 `update_time` datetime DEFAULT NULL COMMENT '更新时间',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

实体类核心注解详解

MP 通过实体类注解完成 实体与数据库表、字段的映射,替代原生 MyBatis 手动映射配置。

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.time.LocalDateTime;

@Data
// 对应数据库表名(若实体类名与表名一致可省略)
@TableName("user")
public class User {
   // 主键ID
   @TableId(type = IdType.AUTO)
   private Long id;
   // 姓名(字段名与数据库一致可省略注解)
   @TableField("name")
   private String name;
   // 年龄
   private Integer age;
   // 邮箱
   private String email;
   // 逻辑删除字段
   @TableLogic
   @TableField(fill = FieldFill.INSERT)
   private Integer deleteFlag;
   // 创建时间(自动填充)
   @TableField(fill = FieldFill.INSERT)
   private LocalDateTime createTime;
   // 更新时间(插入+更新自动填充)
   @TableField(fill = FieldFill.INSERT_UPDATE)
   private LocalDateTime updateTime;
}

当主键属性不叫 id,或需要显式指定主键策略时,应使用 @TableId

主键生成策略

@TableIdtype 属性用于指定主键策略。策略必须与数据库字段类型和表结构保持一致。

主键策略说明适用场景
AUTO使用数据库自增主键,数据库字段必须支持自增。MySQL 单库自增主键。
NONE实体未显式指定策略,按全局配置处理。由项目统一配置主键策略。
INPUT插入前由业务代码手动赋值。业务编码、外部系统主键。
ASSIGN_ID由 MyBatis-Plus 的 IdentifierGenerator 生成数值型 ID。分布式系统中的全局 ID。
ASSIGN_UUID生成不带连字符的 UUID 字符串。字符串主键。

ASSIGN_ID

默认 IdentifierGenerator 使用基于时间、工作节点和序列号的分布式 ID 算法。常见结构可以概括为:

1 位符号位 + 41 位时间戳 + 10 位工作节点标识 + 12 位序列号
  • 时间戳提供大致递增趋势。
  • 工作节点标识用于区分不同进程或服务器。
  • 序列号用于区分同一毫秒内生成的多个 ID。

这种 ID 不是严格连续自增,只能认为整体上趋势递增。分布式部署时仍需关注工作节点冲突和系统时钟回拨;有特殊要求时,可以实现自定义 IdentifierGenerator

@Data
@TableName("user")
public class User {
   @TableId(type = IdType.ASSIGN_ID)
   private Long id;

   private String name;
}

ASSIGN_UUID

ASSIGN_UUID 用于字符串主键。MyBatis-Plus 默认生成不带连字符的 UUID,因此数据库字段通常使用 CHAR(32)VARCHAR(32)

@Data
@TableName("user")
public class User {
   @TableId(type = IdType.ASSIGN_UUID)
   private String id;

   private String name;
}

UUID 的优点是生成简单且不依赖数据库;缺点是字符串占用空间较大、值无序,对聚簇索引的写入局部性不如趋势递增的数值 ID。主键方案应根据数据规模、索引结构和系统架构选择,不应把某一种策略绝对化为所有项目的统一规范。

通用 CRUD

MP 内置 BaseMapperIServiceServiceImpl,可直接完成常见单表 CRUD。复杂查询、联表查询和特殊性能需求仍需要自定义 SQL。

BaseMapper 数据层 CRUD(核心)

Mapper 接口定义

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mp.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
   // 无需编写任何代码,继承BaseMapper即可拥有所有CRUD方法
}

常用 CRUD 方法代码示例

import com.mp.demo.entity.User;
import com.mp.demo.mapper.UserMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.Spring BootTest;
import java.util.List;
@Spring BootTest
public class MapperCrudTest {
   @Autowired
   private UserMapper userMapper;
   // 新增
   @Test
   void testInsert() {
       User user = new User();
       user.setName("张三");
       user.setAge(20);
       user.setEmail("zhangsan@163.com");
       // 返回受影响行数
       int insert = userMapper.insert(user);
       System.out.println("新增ID:" + user.getId());
   }
   // 根据ID查询
   @Test
   void testSelectById() {
       User user = userMapper.selectById(1L);
       System.out.println(user);
   }
   // 查询所有
   @Test
   void testSelectList() {
       List<User> userList = userMapper.selectList(null);
       userList.forEach(System.out::println);
   }
   // 根据ID更新
   @Test
   void testUpdateById() {
       User user = new User();
       user.setId(1L);
       user.setAge(22);
       user.setEmail("update@163.com");
       int rows = userMapper.updateById(user);
       System.out.println("更新行数:" + rows);
   }
   // 根据ID删除
   @Test
   void testDeleteById() {
       int rows = userMapper.deleteById(1L);
       System.out.println("删除行数:" + rows);
   }
}

Service 业务层 CRUD

业务层封装了批量操作和链式调用等能力。Controller 应通过 Service 组织业务,不应直接承担数据访问逻辑。

业务层接口与实现类

// 接口
import com.baomidou.mybatisplus.extension.service.IService;
import com.mp.demo.entity.User;
public interface UserService extends IService<User> {
}
// 实现类
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mp.demo.entity.User;
import com.mp.demo.mapper.UserMapper;
import com.mp.demo.service.UserService;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}

Service 核心方法示例

@Spring BootTest
public class ServiceCrudTest {
   @Autowired
   private UserService userService;
   // 批量新增
   @Test
   void testBatchSave() {
       List<User> list = new ArrayList<>();
       list.add(new User(null, "李四", 25, "lisi@163.com", null, null, null));
       list.add(new User(null, "王五", 28, "wangwu@163.com", null, null, null));
       // 批量插入
       boolean batch = userService.saveBatch(list);
       System.out.println("批量新增结果:" + batch);
   }
   // 新增或更新:主键非空且对应记录存在时更新,否则新增
   @Test
   void testSaveOrUpdate() {
       User user = new User(2L, "李四", 26, "lisi_new@163.com", null, null, null);
       boolean result = userService.saveOrUpdate(user);
   }
   // 批量删除
   @Test
   void testBatchDelete() {
       userService.removeByIds(Arrays.asList(3L,4L));
   }
}

核心重点:条件构造器 Wrapper

Wrapper 用于构造查询和更新条件,可减少简单动态 SQL 的重复代码。复杂 SQL 仍应使用 Mapper XML 或自定义方法。

Wrapper 体系结构

  • QueryWrapper:查询、删除条件构造器(无 set 字段)
  • UpdateWrapper:更新条件构造器(支持 set 字段+条件)
  • LambdaQueryWrapper:Lambda 查询构造器(杜绝硬编码字段名,推荐)
  • LambdaUpdateWrapper:Lambda 更新构造器

QueryWrapper 条件查询示例

// 条件:年龄大于20,姓名包含"李",按年龄降序
@Test
void testQueryWrapper() {
   QueryWrapper<User> wrapper = new QueryWrapper<>();
   wrapper.gt("age", 20)      // age > 20
          .like("name", "李") // name like '%李%'
          .orderByDesc("age");// order by age desc
   List<User> userList = userMapper.selectList(wrapper);
   userList.forEach(System.out::println);
}

LambdaQueryWrapper(推荐,无硬编码)

Lambda Wrapper 通过方法引用获取字段,可减少字符串字段名拼写错误,并提高重构安全性。

// 等价上述条件,无硬编码字段
@Test
void testLambdaQueryWrapper() {
   LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
   wrapper.gt(User::getAge, 20)
          .like(User::getName, "李")
          .orderByDesc(User::getAge);
   List<User> userList = userMapper.selectList(wrapper);
}

UpdateWrapper 动态更新

// 条件:年龄=25,更新邮箱和姓名
@Test
void testUpdateWrapper() {
   LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
   wrapper.eq(User::getAge, 25)
          .set(User::getName, "小李")
          .set(User::getEmail, "xiaoli@163.com");
   userMapper.update(null, wrapper);
}

常用条件方法汇总

方法SQL对应说明
eq=等于
ne!=不等于
gt/lt>/<大于/小于
ge/le>=/<=大于等于/小于等于
likelike ‘%xx%‘模糊查询
inin (xx,xx)包含查询
isNull/isNotNullis null / is not null空值判断

MP 核心高级功能

自动填充功能(创建/更新时间)

业务场景:所有表都有创建时间、更新时间,无需手动赋值,自动填充

实现 MetaObjectHandler 填充处理器

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 {
   // 插入时自动填充
   @Override
   public void insertFill(MetaObject metaObject) {
       this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
       this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
       this.strictInsertFill(metaObject, "deleteFlag", () -> 0, Integer.class);
   }
   // 更新时自动填充
   @Override
   public void updateFill(MetaObject metaObject) {
       this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
   }
}

配合实体类 @TableField(fill = xxx) 注解,实现全自动填充,无需代码赋值。

逻辑删除

逻辑删除不会真正移除数据库记录,而是更新删除标记。配置完成后,MyBatis-Plus 的通用方法会按逻辑删除规则生成 SQL。

全局配置

mybatis-plus:
 global-config:
   db-config:
     logic-delete-field: deleteFlag
     logic-delete-value: 1
     logic-not-delete-value: 0

全局配置中的字段名是实体类属性名。也可以只在单个实体字段上使用 @TableLogic

@TableLogic
private Integer deleteFlag;

常见 SQL 行为如下:

-- 逻辑删除前
DELETE FROM user WHERE id = ?;

-- 逻辑删除后生成的效果
UPDATE user
SET delete_flag = 1
WHERE id = ? AND delete_flag = 0;

-- 普通查询会过滤已删除记录
SELECT id, name, age
FROM user
WHERE delete_flag = 0;

逻辑删除记录默认不会被通用查询和更新方法继续操作。若业务需要频繁查询“已删除”数据,应考虑该字段究竟是删除标记还是普通业务状态,并通过自定义 SQL 明确处理。

分页插件(必备)

分页查询需要注册 PaginationInnerInterceptor,插件会根据数据库方言改写 SQL 并执行总数查询。

分页插件配置类

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MyBatis-PlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus 插件配置
*/
@Configuration
public class MpConfig {
   /**
    * 注册MP核心插件:分页插件 + 乐观锁插件
    */
   @Bean
   public MyBatis-PlusInterceptor mybatisPlusInterceptor() {
       MyBatis-PlusInterceptor interceptor = new MyBatis-PlusInterceptor();
       // 乐观锁插件
       interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
       // 多插件并用时,分页插件通常放在最后
       interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
       return interceptor;
   }
}

分页查询代码示例

@Test
void testPage() {
   // 参数1:当前页,参数2:每页条数
   Page<User> page = new Page<>(1, 2);
   // 分页查询条件
   LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
   wrapper.gt(User::getAge, 18);
   Page<User> userPage = userMapper.selectPage(page, wrapper);
   // 分页结果参数
   System.out.println("当前页:" + userPage.getCurrent());
   System.out.println("每页条数:" + userPage.getSize());
   System.out.println("总条数:" + userPage.getTotal());
   System.out.println("总页数:" + userPage.getPages());
   System.out.println("数据列表:" + userPage.getRecords());
}

乐观锁(解决并发更新问题)

业务场景:多用户同时修改同一条数据,防止数据覆盖丢失

原理

乐观锁通过版本字段判断记录是否已被其他事务修改。更新时携带旧版本号,匹配成功后写入新版本号;匹配失败时,更新行数为 0

实现步骤

  1. 数据库添加 version 字段
ALTER TABLE `user` ADD COLUMN `version` int DEFAULT 1 COMMENT '乐观锁版本号';
  1. 实体类添加版本号注解
@Version
private Integer version;
  1. 开启乐观锁插件(在 MyBatis-PlusInterceptor 中添加)
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());

典型 SQL 效果如下:

UPDATE user
SET age = 22, version = 2
WHERE id = 1 AND version = 1;

乐观锁执行原理流程图

正常更新流程(无并发冲突)
┌─────────┐  1.查询数据  ┌──────────┐
│ 客户端  │─────────────▶│ 数据库   │
└─────────┘              └────┬─────┘
      │                     │
      │ 2.返回version=1     │
      │◀────────────────────┘


┌────────────────────────────┐
│ 3.提交更新:携带id+version  │
└──────────────┬─────────────┘


┌────────────────────────────┐
│ 4.校验version一致,执行更新 │
│ 5.自动version+1 → version=2 │
└──────────────┬─────────────┘


           更新成功
并发冲突流程(多线程同时更新)
┌─────线程1─────┐        ┌─────线程2─────┐
查询version=1           查询version=1
   │                        │
更新携带version=1        更新携带version=1
   │                        │
   ▼                        ▼
执行更新、版本变为2       校验version≠1,更新失败

MyBatis-Plus 底层原理与常见问题

通用 CRUD 的注入过程

项目启动时,MyBatis-Plus 会解析实体的表信息,并通过 SQL Injector 为继承 BaseMapper 的接口注册通用映射语句。运行时调用 Mapper 方法时,仍然进入 MyBatis 的 Mapper 代理、MappedStatement、执行器和 JDBC 流程。

因此,更准确的说法是:通用 CRUD 的语句定义在启动阶段完成注册,实际 SQL 参数、条件片段和插件处理仍会在每次调用时根据当前参数生成和执行。不能简单理解为“所有 SQL 都在启动时固定完成,运行时没有任何拼接成本”。

Wrapper 的执行原理

Wrapper 保存条件片段和参数,调用 Mapper 方法时由 MyBatis-Plus 将这些内容组合到对应的映射语句中,再由 MyBatis 生成 BoundSql 并绑定预编译参数。

普通条件值会使用参数绑定。字段名、排序字段、last()apply() 等可影响 SQL 结构的内容仍需谨慎处理,不能直接使用未经校验的用户输入。

MyBatis 与 MyBatis-Plus 的关系

MyBatis-Plus 基于 MyBatis 扩展,保留 Mapper XML、注解 SQL、插件和类型处理器等原生能力。它主要提供通用 CRUD、Wrapper、分页、逻辑删除、乐观锁、自动填充等功能,两者不是互斥框架。

分页插件未配置

调用 selectPage() 时若未正确注册分页插件,SQL 不会按预期追加数据库分页语句,可能查询出远多于当前页的数据。应检查插件 Bean、数据库类型和依赖版本。

使用多个内部插件时,应注意顺序;分页插件通常放在最后。若升级到 3.5.9 或更高版本,还应按对应版本文档确认是否需要单独引入 JSQLParser 支持模块。

乐观锁常见失效原因

  • 未注册 OptimisticLockerInnerInterceptor
  • 实体的版本字段未添加 @Version
  • 更新前没有读取并携带旧版本号。
  • 使用的方法参数形式不满足插件识别规则。
  • 调用 update(entity, wrapper) 时重复使用同一个 Wrapper。
  • 忽略了更新行数为 0,没有把它作为并发冲突处理。

自动填充常见问题

  • MetaObjectHandler 没有注册为 Spring Bean。
  • 字段未配置正确的 FieldFill
  • 字段已经有值,而 strictInsertFill()strictUpdateFill() 按规则不覆盖。
  • Java 属性名、字段类型与填充代码不一致。

逻辑删除常见问题

  • 全局配置填写的是数据库列名,而不是实体属性名。
  • 数据库默认值、插入填充值和逻辑未删除值不一致。
  • 自定义 SQL 没有按业务需要处理逻辑删除条件。

主键常见问题

  • AUTO 策略对应的数据库列没有设置自增。
  • ASSIGN_UUID 使用字符串主键,但数据库字段长度不足。
  • 自定义 IdentifierGenerator 在多个节点上使用了重复的节点标识。
  • 把趋势递增 ID 误认为严格连续 ID,并依赖其连续性实现业务逻辑。

喜欢的话,留下你的评论吧~

... 访问量
© 2026 跨越星轨的客 @Hoshiumi
Powered by theme astro-koharu · Inspired by Shoka