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。
主键生成策略
@TableId 的 type 属性用于指定主键策略。策略必须与数据库字段类型和表结构保持一致。
| 主键策略 | 说明 | 适用场景 |
|---|---|---|
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 内置 BaseMapper、IService 和 ServiceImpl,可直接完成常见单表 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 | >=/<= | 大于等于/小于等于 |
| like | like ‘%xx%‘ | 模糊查询 |
| in | in (xx,xx) | 包含查询 |
| isNull/isNotNull | is 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。
实现步骤
- 数据库添加 version 字段
ALTER TABLE `user` ADD COLUMN `version` int DEFAULT 1 COMMENT '乐观锁版本号';
- 实体类添加版本号注解
@Version
private Integer version;
- 开启乐观锁插件(在 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,并依赖其连续性实现业务逻辑。
喜欢的话,留下你的评论吧~