如果你完整跟完了前两篇,手动写过 MyBatis 的 XML 映射文件,也配置过复杂的 resultMap,那你应该已经感受到了 MyBatis 的灵活和强大。
但你可能也会偶尔冒出这样一个念头:我只是想查一条数据,为什么要写 XML?我只是想做个简单的分页,为什么要自己拼 LIMIT?
MyBatis-Plus(以下简称 MP)的出现,就是为了回答这个问题。它的官方定位是 "只做增强不做改变" ------不改变 MyBatis 的任何现有行为,只在它上面加一层"便利层"。
这一篇,我们把 MP 的核心功能过一遍,看看它到底"增强"了什么。
学习目标
- 理解 MyBatis-Plus 的定位------只做增强不做改变
- 掌握
BaseMapper提供的通用 CRUD 方法(insert、selectById、updateById、deleteById等) - 掌握条件构造器(
QueryWrapper、UpdateWrapper、LambdaQueryWrapper)的使用 - 理解 MyBatis-Plus 的常见注解(
@TableName、@TableId、@TableField) - 掌握 MyBatis-Plus 的分页、逻辑删除、自动填充等进阶功能
正文
一、MP 的设计哲学:"只做增强不做改变"
MP 的官网上有一句非常关键的话:"MyBatis-Plus 是 MyBatis 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。"
这句话怎么理解?
"不做改变" 意味着:你以前用 MyBatis 写的所有 XML、所有 Mapper 接口、所有配置,在引入 MP 之后完全不受影响。MP 不会覆盖或修改 MyBatis 的任何核心行为。
"只做增强" 意味着:MP 只是在 MyBatis 的"外面"包了一层便利功能------通用 CRUD、条件构造器、分页插件、代码生成器等。这些功能你可以用,也可以不用。
这和很多"大而全"的框架不同。MP 从一开始就定位为 MyBatis 的"补充",而不是"替代"。这也是为什么 MP 的学习曲线相对平缓------你不需要抛弃已有的 MyBatis 知识,只需要在需要的时候"加一点 MP 的用法"就行。
Spring Boot 3.x 的适配 :MP 从 3.5.3+ 版本起正式提供对 Spring Boot 3 的原生兼容能力。如果你使用的是 Spring Boot 3.x,需要引入专门的 mybatis-plus-spring-boot3-starter,而不是普通的 mybatis-plus-boot-starter。目前最新版本为 3.5.16。
二、BaseMapper 的 CRUD 魔法
在原生 MyBatis 中,每个 Mapper 接口都要自己定义方法、自己写 SQL(注解或 XML)。MP 的做法是:提供一个 BaseMapper<T> 接口,你只要继承它,就能自动拥有几十个通用 CRUD 方法。
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 不需要写任何方法,就已经有了 insert、selectById、updateById、deleteById 等
}
这些方法是怎么来的?
MP 在启动时,会扫描所有继承了 BaseMapper 的接口,为每个接口生成对应的代理类。MP 在 BaseMapper 中预定义了所有通用方法的 SQL 模板------比如 selectById 的模板大致是:
xml
<select id="selectById" resultType="T">
SELECT <include refid="Base_Column_List"/> FROM ${tableName}
WHERE ${pkColumn} = #{id}
</select>
真正执行时,MP 会通过反射读取实体类上的注解(@TableName、@TableId 等),把 ${tableName} 替换成真实的表名,把 ${pkColumn} 替换成真实的主键字段。最终生成的 SQL 就是一条完整的查询语句。
BaseMapper 提供了哪些方法?
| 分类 | 典型方法 | 说明 |
|---|---|---|
| 插入 | insert(T entity) |
插入一条记录 |
| 删除 | deleteById(Serializable id)、deleteBatchIds(Collection ids) |
按 ID 删除或批量删除 |
| 修改 | updateById(T entity)、update(T entity, Wrapper wrapper) |
按 ID 更新或按条件更新 |
| 查询 | selectById(Serializable id)、selectList(Wrapper wrapper)、selectPage(Page page, Wrapper wrapper) |
单条查询、列表查询、分页查询 |
这些方法覆盖了 90% 以上的单表 CRUD 场景。你不需要写任何 SQL,不需要写任何 XML,只需要继承 BaseMapper。
三、条件构造器体系:QueryWrapper vs LambdaQueryWrapper
如果说 BaseMapper 解决的是"标准 CRUD"的问题,那条件构造器解决的就是"动态查询条件"的问题。
MP 提供了两类条件构造器:
QueryWrapper:使用字符串表示字段名。
java
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三")
.gt("age", 18)
.like("email", "test");
List<User> users = userMapper.selectList(wrapper);
LambdaQueryWrapper:使用 Lambda 表达式引用字段。
java
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, "张三")
.gt(User::getAge, 18)
.like(User::getEmail, "test");
List<User> users = userMapper.selectList(wrapper);
两者的区别在于:QueryWrapper 用字符串表示字段名,没有编译期检查;LambdaQueryWrapper 用 Lambda 表达式引用实体类的 getter 方法,如果字段名改了,IDE 会提示你重构所有引用。
建议 :优先使用 LambdaQueryWrapper。它类型安全、重构友好,能避免很多因字段名拼写错误导致的运行时异常。QueryWrapper 的唯一优势是灵活性更高------当你需要动态拼接列名(比如排序字段来自用户输入)时,QueryWrapper 比 Lambda 方式更方便。
UpdateWrapper 的作用类似,但专门用于更新操作------它既可以设置 WHERE 条件,也可以设置 SET 的字段值。
四、核心注解:让实体类和数据库表"对上号"
MP 通过注解来建立实体类和数据库表之间的映射关系。最常用的三个注解是:
@TableName:指定实体类对应的数据库表名。
java
@TableName("t_user")
public class User {
// ...
}
如果实体类名和表名一致(比如 User 对应 user 表),这个注解可以省略。MP 默认会使用类名的下划线形式作为表名(User → user)。
@TableId:指定主键字段。
java
@TableId(value = "user_id", type = IdType.AUTO)
private Integer id;
value:数据库表中的主键列名type:主键生成策略,常用的是IdType.AUTO(数据库自增)和IdType.ASSIGN_ID(MP 自动分配雪花算法 ID)
如果数据库主键列名就是 id,且你想用数据库自增,这个注解也可以省略------MP 会默认把名为 id 的字段当作主键。
@TableField:指定普通字段的映射关系。
java
@TableField("user_name")
private String username;
@TableField(exist = false)
private String extraInfo; // 这个字段在数据库中不存在
@TableField 的常见用法:
- 字段名映射:数据库列名和 Java 属性名不一致时,用
value指定 - 排除非表字段:
exist = false表示该属性不是数据库字段 - 自动填充:
fill = FieldFill.INSERT表示插入时自动填充(后面会详细讲)
五、进阶功能:分页、逻辑删除、自动填充
这三个功能是 MP 在企业级项目中使用频率最高的"增强特性"。
分页插件
MP 的分页插件 PaginationInnerInterceptor 提供了强大的分页能力,支持多种数据库。
配置方式(Spring Boot 3.x):
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
使用方式:
java
// 创建分页对象:第 1 页,每页 10 条
Page<User> page = new Page<>(1, 10);
// 执行分页查询
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.ge(User::getAge, 18);
Page<User> result = userMapper.selectPage(page, wrapper);
// 获取结果
List<User> records = result.getRecords(); // 当前页数据
long total = result.getTotal(); // 总记录数
分页插件会自动拦截 SQL,在原来查询的基础上拼接 LIMIT 语句,并单独执行 COUNT 查询获取总记录数。如果不需要 COUNT 查询,可以设置 page.setSearchCount(false)。
逻辑删除
逻辑删除(Logic Delete)是指不在数据库中真正删除记录,而是通过一个标记字段来标识"已删除" 。这样数据可以恢复,也可以追溯历史。
MP 的逻辑删除功能会在执行数据库操作时自动处理逻辑删除字段:
- 删除:将 DELETE 操作转换为 UPDATE 操作,标记记录为已删除
- 查询:自动添加条件,过滤掉标记为已删除的记录
配置方式:
在 application.yml 中配置全局逻辑删除属性:
yaml
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 逻辑删除字段名
logic-delete-value: 1 # 已删除的值
logic-not-delete-value: 0 # 未删除的值
或者在实体类字段上使用 @TableLogic 注解:
java
@TableLogic
private Integer deleted;
默认情况下,逻辑未删除值为 0,已删除值为 1。你也可以通过注解的 value 和 delval 属性自定义。
注意事项 :MP 的逻辑删除会自动在查询和更新中过滤已删除数据。但如果业务中有"需要查询已删除数据"的场景,需要绕过这个过滤------可以自己写 SQL,或者在查询时显式指定 ew.apply("deleted = 1")。
自动填充
在业务开发中,create_time、update_time、create_by 这类字段几乎每张表都有。每次插入或更新时手动设置,既繁琐又容易遗漏。
MP 的自动填充功能可以帮你自动处理这些字段。
第一步:在实体类中标记需要自动填充的字段:
java
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
FieldFill 的几种策略:
INSERT:插入时填充UPDATE:更新时填充INSERT_UPDATE:插入和更新时都填充
第二步:实现 MetaObjectHandler 接口:
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());
}
}
自动填充功能需要确保处理器类被 Spring 管理(使用 @Component 或 @Bean)。如果在插入或更新时字段已经有值,MP 默认不会覆盖。
代码示例
示例一:BaseMapper 零 SQL 完成 CRUD
依赖(pom.xml) :
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.16</version>
</dependency>
实体类:
java
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import java.time.LocalDateTime;
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Integer id;
private String username;
private String email;
private Integer age;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
// getter / setter 省略
}
Mapper 接口:
java
package com.example.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.demo.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// 不需要写任何方法------BaseMapper 已经提供了 insert、selectById、updateById、deleteById
}
Service 层使用:
java
package com.example.demo.service;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final UserMapper userMapper;
public UserService(UserMapper userMapper) {
this.userMapper = userMapper;
}
// 插入
public void save(User user) {
userMapper.insert(user);
System.out.println("插入后主键: " + user.getId());
}
// 按 ID 查询
public User findById(Integer id) {
return userMapper.selectById(id);
}
// 查询所有
public List<User> findAll() {
return userMapper.selectList(null); // null 表示无条件
}
// 按 ID 更新
public void update(User user) {
userMapper.updateById(user);
}
// 按 ID 删除
public void deleteById(Integer id) {
userMapper.deleteById(id);
}
}
关键观察 :整个 UserMapper 接口没有任何方法和 SQL,但 UserService 中已经可以调用 insert、selectById、updateById、deleteById 等方法。这些方法全部来自 BaseMapper 的"增强"。
示例二:复杂条件查询(LambdaQueryWrapper)
java
package com.example.demo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserQueryService {
private final UserMapper userMapper;
public UserQueryService(UserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 多条件查询:年龄大于 18 且姓名包含"张"的用户
*/
public List<User> findAdultsByName(String nameKeyword) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.ge(User::getAge, 18)
.like(User::getUsername, nameKeyword)
.orderByDesc(User::getCreateTime);
return userMapper.selectList(wrapper);
}
/**
* 动态条件查询:所有参数都是可选的
*/
public List<User> dynamicQuery(String username, Integer minAge, Integer maxAge) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
// 只有 username 不为空时才加这个条件
wrapper.like(username != null && !username.isEmpty(), User::getUsername, username);
// 只有 minAge 不为空时才加这个条件
wrapper.ge(minAge != null, User::getAge, minAge);
// 只有 maxAge 不为空时才加这个条件
wrapper.le(maxAge != null, User::getAge, maxAge);
return userMapper.selectList(wrapper);
}
/**
* 按条件更新:将指定年龄段的用户邮箱统一更新
*/
public void updateEmailByAge(String newEmail, Integer minAge, Integer maxAge) {
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
wrapper.ge(minAge != null, User::getAge, minAge)
.le(maxAge != null, User::getAge, maxAge);
User updateEntity = new User();
updateEntity.setEmail(newEmail);
userMapper.update(updateEntity, wrapper);
}
}
Wrappers.lambdaQuery() 是创建 LambdaQueryWrapper 的便捷方法。链式调用中的每个方法都支持一个 condition 参数------第一个参数是布尔值,为 true 时才添加该条件。这是处理动态查询的推荐方式,比手写 if 判断更简洁。
新手错误 vs 正确姿势
| 错误表象 | 根本原因 | 正确姿势 |
|---|---|---|
使用 QueryWrapper 硬编码字段名,实体类字段改名后查询条件失效但编译不报错 |
未使用 LambdaQueryWrapper,字符串字段名没有编译期检查 |
优先使用 LambdaQueryWrapper,通过 User::getName 引用字段 |
| 逻辑删除后查询仍能查到已删除数据,逻辑删除"不生效" | 未配置逻辑删除的全局规则,或配置了但 Mapper 方法没有使用 MP 提供的 deleteById |
在 application.yml 中配置 logic-delete-field 和对应的值,删除时使用 deleteById 而非自定义 SQL |
自动填充不生效,createTime 和 updateTime 始终为 null |
未实现 MetaObjectHandler,或处理器类未被 Spring 管理 |
创建 MetaObjectHandler 的实现类并加上 @Component |
分页查询返回的 total 为 0,但数据库中有数据 |
分页插件未正确配置,或 DbType 与数据库类型不匹配 |
在 MybatisPlusInterceptor 中添加 PaginationInnerInterceptor 并指定正确的 DbType |
在 Spring Boot 3.x 中引入 MP 后启动报 factoryBeanObjectType 类型错误 |
使用了适配 Spring Boot 2.x 的 mybatis-plus-boot-starter,API 不兼容 |
使用专门适配 Spring Boot 3.x 的 mybatis-plus-spring-boot3-starter |
疑难深度追问
Q1:MyBatis-Plus 的 IService 和 BaseMapper 有什么区别?
BaseMapper 是 Mapper 层的增强,提供的是数据访问方法(CRUD)。IService 是 Service 层的增强,在 BaseMapper 的基础上提供了更多批量操作 (如 saveBatch、updateBatchById)和链式调用 (如 lambdaQuery().eq(...).list())。
IService 的典型用法是:让你的 Service 接口继承 IService<T>,实现类继承 ServiceImpl<M extends BaseMapper<T>, T>。这样 Service 层就直接拥有了批量操作和链式查询能力。但需要说明的是,IService 是对 Service 层的增强,并非必须使用------你完全可以只使用 BaseMapper,自己写 Service 逻辑。
Q2:如果 MP 的自动生成 SQL 不能满足需求,如何自定义?
MP 的"只做增强不做改变"意味着:你可以在 Mapper 中直接写方法和 SQL,MP 不会覆盖或干扰。
java
@Mapper
public interface UserMapper extends BaseMapper<User> {
// MP 自动生成的方法照常可用
// 自定义方法------和原生 MyBatis 完全一样
@Select("SELECT * FROM t_user WHERE age > #{age}")
List<User> selectByAge(int age);
}
MP 的自动生成和手写 SQL 可以共存。手写 SQL 的场景包括:多表关联查询、复杂统计、存储过程调用等。
Q3:MP 的"只做增强不做改变"意味着什么?如果要升级 MyBatis 版本,MP 是否会造成阻碍?
"只做增强不做改变"的核心含义是:MP 不修改 MyBatis 的任何核心类或行为,所有增强功能都是通过 MyBatis 的插件机制(Interceptor)和动态代理机制实现的。这意味着:
- 你原有的 MyBatis XML 和 Mapper 完全不受影响
- MP 和原生 MyBatis 可以无缝共存
- 升级 MyBatis 版本时,只要 MP 对应版本做了适配,就不会有问题
但有一点需要注意:MP 对 MyBatis 的版本有明确的要求,升级 MyBatis 主版本时需要确认 MP 是否已经适配。MP 通常会紧跟 MyBatis 的版本更新,但会有一定的延迟。
思考与延伸
-
动手对比 :选一个你之前用原生 MyBatis 写过的单表 CRUD 功能,用 MP 的
BaseMapper重写一遍。对比两种方式的代码行数和开发时间。 -
思考题 :MP 的
LambdaQueryWrapper使用 Lambda 表达式引用User::getName。这个语法在 Java 8 中是如何实现的?User::getName本质上是什么类型?提示:SerializedLambda。 -
延伸阅读:MP 的官方文档对"代码生成器"有详细介绍------它可以自动根据数据库表结构生成 Entity、Mapper、Service、Controller 的完整代码。如果你的项目中有大量"纯单表操作",代码生成器可以大幅提升开发效率。
参考与延伸阅读
- Baomidou. MyBatis-Plus 官方文档. baomidou.com
- Baomidou. MyBatis-Plus 逻辑删除支持. baomidou.com, 2025-09-09
- Baomidou. MyBatis-Plus 分页插件. baomidou.com, 2025-09-30
- Baomidou. MyBatis-Plus 自动填充字段. baomidou.com, 2025-03-05
- 阿里云开发者社区. MyBatis-Plus 常见注解详解. 2025-12-30
- CSDN. 手把手带你拆解MyBatis-Plus BaseMapper底层原理. 2025-07-16
- 腾讯云. LambdaQueryWrapper 让你的 MyBatis-Plus 查询更安全、更优雅. 2025-06-01