MyBatis-Plus 从入门到实战:常用功能与核心原理

前言

在 Java 后端开发中,MyBatis 是非常常见的持久层框架。

不过实际开发中,我们经常会写大量重复的单表 CRUD,例如:

java 复制代码
insert
deleteById
updateById
selectById
selectList

这些代码在不同项目中往往都差不多。

MyBatis-Plus,简称 MP,就是在 MyBatis 基础上进行增强的工具。

它的核心理念是:

只做增强,不做改变。

也就是说,简单 CRUD 可以交给 MyBatis-Plus,复杂 SQL 依然可以继续使用 MyBatis XML。

简单理解:

text 复制代码
MyBatis-Plus
=
MyBatis
+
通用 CRUD
+
条件构造器
+
分页
+
逻辑删除
+
自动填充
+
常用插件

下面主要介绍日常开发中最常使用的一些功能。


一、基本使用

在 Spring Boot 项目中引入 MyBatis-Plus 后,假设有一张用户表:

sql 复制代码
CREATE TABLE user (
    id BIGINT PRIMARY KEY,
    username VARCHAR(50),
    age INT,
    status INT,
    deleted INT DEFAULT 0,
    version INT DEFAULT 1,
    create_time DATETIME,
    update_time DATETIME
);

对应实体类:

java 复制代码
@Data
@TableName("user")
public class User {

    @TableId
    private Long id;

    private String username;

    private Integer age;

    private Integer status;

    private Integer deleted;

    private Integer version;

    private LocalDateTime createTime;

    private LocalDateTime updateTime;
}

常见注解包括:

text 复制代码
@TableName   表名映射
@TableId     主键
@TableField  字段映射
@TableLogic  逻辑删除
@Version     乐观锁

二、BaseMapper:简化 CRUD

创建 Mapper:

java 复制代码
@Mapper
public interface UserMapper extends BaseMapper<User> {
}

只需要继承:

java 复制代码
BaseMapper<User>

就可以直接使用常见 CRUD。

新增

java 复制代码
User user = new User();
user.setUsername("张三");
user.setAge(20);

userMapper.insert(user);

根据 ID 查询

java 复制代码
User user = userMapper.selectById(1L);

修改

java 复制代码
User user = new User();
user.setId(1L);
user.setUsername("李四");

userMapper.updateById(user);

删除

java 复制代码
userMapper.deleteById(1L);

查询全部

java 复制代码
List<User> users = userMapper.selectList(null);

这也是 MyBatis-Plus 最直观的优势:

很多简单 SQL 不需要重复手写。


三、IService

实际项目一般会按照:

text 复制代码
Controller
    ↓
Service
    ↓
Mapper
    ↓
Database

这样的结构开发。

Service 可以继承:

java 复制代码
public interface UserService extends IService<User> {
}

实现类:

java 复制代码
@Service
public class UserServiceImpl
        extends ServiceImpl<UserMapper, User>
        implements UserService {
}

之后可以直接使用:

java 复制代码
userService.save(user);

userService.getById(1L);

userService.list();

userService.removeById(1L);

userService.updateById(user);

还有:

java 复制代码
saveBatch()
saveOrUpdate()
page()
count()

不过复杂业务还是建议定义语义明确的方法,例如:

java 复制代码
freezeUser();

cancelOrder();

refundOrder();

而不是所有业务都直接调用通用 CRUD。


四、QueryWrapper 条件查询

MyBatis-Plus 中非常常用的一个功能就是 Wrapper。

例如查询:

年龄大于等于 18,并且状态正常的用户。

可以写:

java 复制代码
QueryWrapper<User> wrapper = new QueryWrapper<>();

wrapper.ge("age", 18)
       .eq("status", 1);

List<User> list =
        userMapper.selectList(wrapper);

常见条件:

text 复制代码
eq       =
ne       !=
gt       >
ge       >=
lt       <
le       <=
like     LIKE
in       IN
between  BETWEEN

还可以:

java 复制代码
orderByAsc();
orderByDesc();

进行排序。


五、更推荐 LambdaQueryWrapper

普通 QueryWrapper 有一个问题:

java 复制代码
wrapper.eq("username", "张三");

字段名是字符串。

如果以后字段发生变化,IDE 重构不一定能够发现这里的问题。

因此实际项目中更推荐:

java 复制代码
LambdaQueryWrapper<User> wrapper =
        new LambdaQueryWrapper<>();

wrapper.eq(User::getUsername, "张三")
       .ge(User::getAge, 18)
       .eq(User::getStatus, 1);

List<User> list =
        userMapper.selectList(wrapper);

相比:

java 复制代码
"username"

这种写法:

java 复制代码
User::getUsername

更加安全,也更方便重构。

所以我一般更推荐:

text 复制代码
LambdaQueryWrapper
LambdaUpdateWrapper

六、动态查询

Wrapper 很适合开发搜索接口。

例如前端传:

java 复制代码
String username;
Integer status;
Integer age;

可以写:

java 复制代码
LambdaQueryWrapper<User> wrapper =
        new LambdaQueryWrapper<>();

wrapper
    .like(
        StringUtils.hasText(username),
        User::getUsername,
        username
    )
    .eq(
        status != null,
        User::getStatus,
        status
    )
    .ge(
        age != null,
        User::getAge,
        age
    );

当条件不满足时,对应 SQL 条件不会被拼接。

相比大量:

java 复制代码
if (...) {
    wrapper.eq(...);
}

代码会更加简洁。


七、LambdaUpdateWrapper

除了查询,Wrapper 也可以用于更新。

例如:

禁用 id 为 1 的用户。

java 复制代码
LambdaUpdateWrapper<User> wrapper =
        new LambdaUpdateWrapper<>();

wrapper.eq(User::getId, 1L)
       .set(User::getStatus, 0);

userMapper.update(null, wrapper);

最终 SQL 类似:

sql 复制代码
UPDATE user
SET status = 0
WHERE id = 1;

对于只修改少数字段的情况,这种方式比较方便。


八、分页查询

后台管理系统通常都会使用分页。

配置:

java 复制代码
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {

        MybatisPlusInterceptor interceptor =
                new MybatisPlusInterceptor();

        interceptor.addInnerInterceptor(
            new PaginationInnerInterceptor(DbType.MYSQL)
        );

        return interceptor;
    }
}

查询:

java 复制代码
Page<User> page = new Page<>(1, 10);

Page<User> result =
        userMapper.selectPage(page, null);

其中:

text 复制代码
1  当前页
10 每页条数

分页结果可以获取:

java 复制代码
result.getRecords();
result.getTotal();
result.getCurrent();
result.getSize();
result.getPages();

如果带条件:

java 复制代码
LambdaQueryWrapper<User> wrapper =
        new LambdaQueryWrapper<>();

wrapper.eq(User::getStatus, 1)
       .orderByDesc(User::getCreateTime);

Page<User> result =
        userMapper.selectPage(
            new Page<>(1, 10),
            wrapper
        );

九、逻辑删除

实际项目中,有些数据不能真正删除。

例如用户、文章、订单等数据可能需要保留历史记录。

这时可以使用逻辑删除。

数据库字段:

text 复制代码
deleted

0 = 正常
1 = 已删除

实体中:

java 复制代码
@TableLogic
private Integer deleted;

执行:

java 复制代码
userMapper.deleteById(1L);

表面上是删除,实际 SQL 类似:

sql 复制代码
UPDATE user
SET deleted = 1
WHERE id = 1;

之后正常查询时,会自动过滤已经逻辑删除的数据。

需要注意:

逻辑删除和业务状态不是一回事。

例如订单:

text 复制代码
待支付
已支付
已取消
已完成

应该使用 status 表示,而不是使用 deleted。


十、自动填充

很多数据库表都有:

text 复制代码
create_time
update_time

如果每次手动赋值会比较麻烦。

可以使用 MyBatis-Plus 自动填充。

实体:

java 复制代码
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;

@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

然后实现:

java 复制代码
@Component
public class MyMetaObjectHandler
        implements MetaObjectHandler {

    @Override
    public void insertFill(MetaObject metaObject) {

        this.strictInsertFill(
            metaObject,
            "createTime",
            LocalDateTime.class,
            LocalDateTime.now()
        );

        this.strictInsertFill(
            metaObject,
            "updateTime",
            LocalDateTime.class,
            LocalDateTime.now()
        );
    }

    @Override
    public void updateFill(MetaObject metaObject) {

        this.strictUpdateFill(
            metaObject,
            "updateTime",
            LocalDateTime.class,
            LocalDateTime.now()
        );
    }
}

除了时间,还可以用于:

text 复制代码
createUser
updateUser

等公共字段。


十一、乐观锁

假设两个用户同时修改同一条数据。

用户 A 查询到:

text 复制代码
version = 1

用户 B 查询时也是:

text 复制代码
version = 1

如果两个人同时提交修改,可能发生数据覆盖。

MyBatis-Plus 可以使用乐观锁。

实体:

java 复制代码
@Version
private Integer version;

配置:

java 复制代码
interceptor.addInnerInterceptor(
    new OptimisticLockerInnerInterceptor()
);

更新时 SQL 思路类似:

sql 复制代码
UPDATE user
SET username = '新的名字',
    version = 2
WHERE id = 1
AND version = 1;

如果其他线程已经把版本改成:

text 复制代码
version = 2

那么:

text 复制代码
version = 1

这个条件就无法匹配,从而发现并发冲突。


十二、MybatisPlusInterceptor

前面的分页、乐观锁,其实都属于 MyBatis-Plus 的插件能力。

核心入口是:

java 复制代码
MybatisPlusInterceptor

例如:

java 复制代码
MybatisPlusInterceptor interceptor =
        new MybatisPlusInterceptor();

interceptor.addInnerInterceptor(
    new PaginationInnerInterceptor()
);

interceptor.addInnerInterceptor(
    new OptimisticLockerInnerInterceptor()
);

除了这些,还可以实现:

text 复制代码
多租户
动态表名
数据权限
防止全表更新和删除

等功能。

因此 MyBatis-Plus 不只是简单地帮我们少写 CRUD,也提供了很多数据库访问层的公共能力。


十三、BaseMapper 为什么没有实现类?

我们只写:

java 复制代码
public interface UserMapper
        extends BaseMapper<User> {
}

却没有:

text 复制代码
UserMapperImpl

为什么可以执行?

这里可以简单理解为两个部分。

1. MyBatis 动态代理

MyBatis 会在运行时为 Mapper 接口创建代理对象。

所以:

java 复制代码
userMapper.selectById(1L);

实际上调用的是 MyBatis 生成的代理对象。

2. MyBatis-Plus SQL Injector

MyBatis-Plus 在启动时会把:

text 复制代码
insert
deleteById
updateById
selectById
selectList

这些通用 CRUD 注册进去。

整体流程可以简单理解为:

text 复制代码
UserMapper
    ↓
BaseMapper
    ↓
MyBatis-Plus 注册通用 SQL
    ↓
MyBatis Mapper 动态代理
    ↓
Executor
    ↓
JDBC
    ↓
数据库

所以 MyBatis-Plus 底层依然建立在 MyBatis 之上。


十四、复杂 SQL 应该怎么办?

使用 MyBatis-Plus 之后,一个常见误区是:

是不是以后都不用写 SQL 了?

答案当然不是。

例如:

sql 复制代码
SELECT *
FROM user
WHERE status = 1;

这种简单查询非常适合 Wrapper。

但是如果涉及:

text 复制代码
多表 JOIN
GROUP BY
复杂聚合
子查询
统计报表
窗口函数

如果强行使用 Wrapper 拼接,可读性可能反而下降。

这时候直接使用:

text 复制代码
Mapper + XML

通常更加清晰。

所以我个人更推荐:

text 复制代码
简单 CRUD
    ↓
BaseMapper / IService

动态条件
    ↓
LambdaQueryWrapper

动态更新
    ↓
LambdaUpdateWrapper

复杂 SQL
    ↓
Mapper XML

十五、实际开发中的几个注意点

1. 尽量使用 Lambda Wrapper

少写:

java 复制代码
wrapper.eq("username", username);

推荐:

java 复制代码
wrapper.eq(
    User::getUsername,
    username
);

可以减少字段名写错的问题。


2. 不要直接相信前端传来的 SQL 字段

例如:

java 复制代码
wrapper.orderByAsc(sortField);

如果 sortField 完全由前端控制,就存在安全风险。

更合理的方式是做字段白名单:

text 复制代码
username
createTime
id

只允许指定字段参与排序。


3. 不要滥用 saveOrUpdate

java 复制代码
saveOrUpdate(user);

确实方便。

但是很多业务应该明确区分:

text 复制代码
新增
修改

例如:

java 复制代码
createUser();

updateUser();

业务语义会更加清晰。


4. Wrapper 太复杂时直接写 SQL

如果 Wrapper 已经写成:

java 复制代码
.and(...)
.or(...)
.nested(...)
.exists(...)

并且自己都很难一眼看懂最终 SQL,那么通常直接写 XML 会更加合适。

工具是为了降低复杂度,而不是增加复杂度。


十六、总结

MyBatis-Plus 本质上是对 MyBatis 的增强。

它比较适合解决:

text 复制代码
CRUD
分页
动态查询
逻辑删除
自动填充
乐观锁

这些大量重复出现的开发需求。

常用学习路线可以概括为:

text 复制代码
BaseMapper
    ↓
IService
    ↓
LambdaQueryWrapper
    ↓
LambdaUpdateWrapper
    ↓
分页
    ↓
逻辑删除
    ↓
自动填充
    ↓
乐观锁
    ↓
MybatisPlusInterceptor

但 MyBatis-Plus 并不是为了完全取代 SQL。

比较合理的使用方式应该是:

text 复制代码
简单 CRUD
交给 MyBatis-Plus

复杂 SQL
交给 MyBatis

业务逻辑
放在 Service

数据一致性
依靠数据库设计和事务

我认为 MyBatis-Plus 最大的价值不是:

让我们以后不用写 SQL。

而是:

让我们不用再重复写那些没有必要手写的 SQL。

理解这一点之后,MyBatis-Plus 才真正算是用对了。

相关推荐
会编程的吕洞宾1 小时前
Spring AI 2.0 会话记忆实战,让 AI 助手记住每一次对话
java·人工智能·spring
小宋10211 小时前
Agent 工具升级如何不破坏线上:Tool Schema 版本兼容与契约测试
java·人工智能·后端·spring
ss2731 小时前
AI全栈实战 | 3.6-01 数据库与 ORM:学过 MyBatis 再看 SQLAlchemy,才知道“半自动“和“全自动“差在哪
数据库·mybatis
汤米粥1 小时前
后端开发主流技术方案
java·python·golang·php·nodejs·后端开发
Helix2501 小时前
2026编程入门指南:零基础如何科学选择第一门编程语言?
java·c语言·javascript·零基础·编程语言·python入门·编程就业
EatFan1 小时前
2026 后端 AI 集成三条落地路径:Spring AI 2.0、MCP 协议与旁路服务怎么选
java·人工智能·spring boot·spring·大模型·spring ai·mcp
SimonKing1 小时前
QClaw关停之后:一个工具的退场,一段关系的告别
java·后端·程序员
秋天的一阵风1 小时前
🚀 nacos-web-config:运营半夜改条配置,网页秒更新 —— 不用发版、不用轮询,我把它开源了
java·前端·开源
for_ever_love__1 小时前
缓存穿透、击穿、雪崩:三个经典问题与完整解决方案
java·数据库·redis·缓存·哈希算法·布隆过滤器·雪崩