Skip to content
概述
MyBatis-Plus(MP)是在 MyBatis 基础上的增强层。它不修改 MyBatis 核心,也不 fork 源码,与原生 MyBatis 完全兼容。一个项目中可以同时存在 MP 与 MyBatis XML:单表 CRUD 由 MP 处理,多表关联查询或复杂 SQL 仍使用 MyBatis XML 映射。
两者不是替代关系。MyBatis 负责 SQL 与对象映射的基础能力;MP 在此基础上提供了更高层的封装,主要消除重复的单表操作编写。理解它们在机制层面的差异,有助于确定在什么场景下使用哪一侧的能力。
基本概念
体系结构
MP 在 MyBatis 原有的 SqlSession → Executor → StatementHandler → ParameterHandler/ResultSetHandler 调用链上叠加了四个抽象层:
Service 层 (IService / ServiceImpl) → 批量操作、事务封装
Mapper 层 (BaseMapper<T>) → 零 XML 单表 CRUD
SQL 注入层 (AbstractMethod) → 自动生成 MappedStatement
元数据层 (TableInfo / TableFieldInfo) → 实体-表映射缓存每一层都可以单独替换。例如重写 ServiceImpl.saveBatch 的默认实现(默认是逐条 INSERT)时,不会影响 Mapper 层与 SQL 注入层的行为。
与原生 MyBatis 的核心区别
| 维度 | 原生 MyBatis | MyBatis-Plus |
|---|---|---|
| 单表 CRUD | 需要手动编写 XML 或注解 SQL | 继承 BaseMapper 即可获得,零 XML |
| 条件构造器 | 通过 XML 动态 SQL 或手写条件逻辑 | 提供 QueryWrapper / LambdaQueryWrapper |
| 分页 | 需要自己实现拦截器或在 SQL 中拼装 | 内置分页插件,配置即可物理分页 |
| 主键策略 | 手动指定或通过 selectKey 实现 | 内置多种 IdType(AUTO、雪花算法等) |
| 代码生成器 | 无内置工具 | 提供代码生成器,可生成 Mapper/Service/Controller 骨架 |
| 逻辑删除 | 需手动修改 SQL 和结果映射 | 注解配置后自动改写 DELETE 为 UPDATE |
| 多租户/动态表名 | 需通过插件自行实现 | 内置插件,按规则自动追加条件或替换表名 |
| 执行控制 | 完全由使用者控制 SQL 编写 | 对单表操作高度抽象,但也需要理解 SQL 注入过程才能定制 |
这种区别决定了:MP 并不会替代 MyBatis,而是在单表操作这片重复性极高的区域提供自动化。多表映射、存储过程等场景,依然需要直接使用 MyBatis。
工作原理
SQL 注入:CRUD 方法自动生成
Spring Boot 启动时会触发注入过程:
MybatisPlusAutoConfiguration
→ MybatisSqlSessionFactoryBean.buildSqlSessionFactory()
→ MybatisMapperRegistry → MapperAnnotationBuilder.parse()
→ MybatisPlusInjector.inspectInject()
→ 遍历所有 AbstractMethod 子类
→ SelectById.injectMappedStatement()每个内置 CRUD 方法都对应一个 AbstractMethod 子类。以 SelectById 为例:
java
public class SelectById extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(
Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.SELECT_BY_ID;
SqlSource sqlSource = super.createSqlSource(configuration,
String.format(sqlMethod.getSql(),
sqlSelectColumns(tableInfo, false),
tableInfo.getTableName(),
tableInfo.getKeyColumn(),
tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true)
), Object.class);
return this.addSelectMappedStatementForTable(
mapperClass, methodName, sqlSource, tableInfo);
}
}SQL 模板定义在 SqlMethod 枚举中,占位符由运行时解析好的 TableInfo 填充。这是模板方法模式:子类提供具体模板和方法名,父类负责缓存、注册以及 ResultMap 的构建。
若要添加全局自定义方法,只需编写 AbstractMethod 子类并将其注册到注入器,所有 Mapper 就会自动获得新的方法。原生 MyBatis 在这一层没有任何自动生成机制。
TableInfo 缓存
TableInfo 缓存实体类元数据,结构如下:
java
public class TableInfo {
private Class<?> entityType;
private String tableName;
private String keyColumn;
private String keyProperty;
private IdType idType;
private List<TableFieldInfo> fieldList;
private boolean withLogicDelete;
}启动阶段根据 @TableName、@TableId、@TableField 等注解解析映射信息,并存入静态缓存 TABLE_INFO_CACHE(Map<Class<?>, TableInfo>),运行时查询不再使用反射。
运行时更改表名或字段映射并不会刷新缓存。动态表名插件的替换发生在 SQL 生成的下游,不依赖缓存。
LambdaQueryWrapper 的字段名解析
LambdaQueryWrapper 通过方法引用获得字段名,例如 qw.eq(Book::getId, 2) 最终得到 id。调用路径如下:
AbstractWrapper.eq
→ AbstractLambdaWrapper.columnToString
→ LambdaUtils.extract解析过程使用三种策略依次降级:
java
public static <T> LambdaMeta extract(SFunction<T, ?> func) {
if (func instanceof Proxy) // 1. IDEA 调试模式
return new IdeaProxyLambdaMeta((Proxy) func);
try {
Method m = func.getClass().getDeclaredMethod("writeReplace");
return new ReflectLambdaMeta(...); // 2. 反射(主路径)
} catch (Throwable e) {
return new ShadowLambdaMeta(...); // 3. 序列化降级
}
}SFunction 继承了 Function 和 Serializable。编译器会为 Lambda 自动生成 writeReplace 方法,其返回的 SerializedLambda 中包含 implMethodName,也就是原始方法名(如 getId)。随后通过 PropertyNamer.methodToProperty 去掉 get 前缀并小写首字母,得到属性名 id。解析结果以弱引用的形式缓存。
支持的方法引用形式
java
qw.eq(Book::getId, 2); // 编译期确定方法引用,可正常工作
qw.eq(b -> b.getId(), 2); // 不支持,implMethodName 为编译器合成名
qw.eq(new SFunction<Book,Long>(){}); // 不支持匿名内部类,缺少 writeReplace原生 MyBatis 没有类似机制,字段名通常以字符串形式硬编码在 XML 或注解中,重命名时需要手动修改。
插件体系
MP 复用 MyBatis 的 Interceptor 机制,提供了一系列内置插件:
| 插件 | 拦截点 | 行为 |
|---|---|---|
| 分页 | Executor.query() | 自动拼接物理分页 SQL |
| 乐观锁 | Executor.update() | 检查并自增版本号 |
| 逻辑删除 | SQL 注入阶段 | 将 DELETE 改写为 UPDATE |
| 多租户 | SQL 解析之后 | 追加租户过滤条件 |
| 动态表名 | SQL 构建时 | 运行时替换目标表名 |
分页插件必须显式配置 PaginationInnerInterceptor,否则程序会执行内存分页——先查出全部数据再截取,数据量大时可能导致内存溢出。当多个插件同时启用时,执行顺序由配置决定,框架不保证默认顺序;顺序错误可能导致 SQL 语法或语义异常。
常用 API
BaseMapper<T>
BaseMapper 提供了最基础的 CRUD 方法,继承后直接可用。列表中的方法均与 MyBatis 映射兼容,返回值表示影响行数。
| 方法 | 描述 |
|---|---|
int insert(T entity) | 插入一条记录,主键回填至实体 |
int deleteById(Serializable id) | 根据主键删除 |
int deleteBatchIds(Collection<?> idList) | 批量主键删除 |
int updateById(T entity) | 根据主键更新,仅更新非 null 字段(取决于更新策略) |
T selectById(Serializable id) | 根据主键查询 |
List<T> selectBatchIds(Collection<?> idList) | 根据主键批量查询 |
List<T> selectByMap(Map<String, Object> columnMap) | 按字段等值条件查询 |
List<T> selectList(Wrapper<T> queryWrapper) | 根据条件构造器查询列表 |
Page<T> selectPage(Page<T> page, Wrapper<T> queryWrapper) | 分页查询(需配置分页插件) |
IService<T>
IService 在 Mapper 之上提供了更具业务语义的方法,返回值类型有所调整。以下列出常用方法。
| 方法 | 描述 |
|---|---|
boolean save(T entity) | 插入记录,返回 true 表示成功 |
boolean saveBatch(Collection<T> entityList) | 批量插入(默认逐条 INSERT,可重写) |
boolean removeById(Serializable id) | 根据主键删除 |
boolean removeBatchByIds(Collection<?> idList) | 批量删除 |
boolean updateById(T entity) | 根据主键更新 |
T getById(Serializable id) | 根据主键查询 |
IPage<T> page(IPage<T> page, Wrapper<T> queryWrapper) | 分页查询 |
BaseMapper 和 IService 提供了一组功能相似的接口——例如 deleteById 返回 int,而 removeById 返回 boolean,它们分属不同的抽象层级。团队内部应统一约定调用层级,避免在同一模块中混用两种风格。
条件构造器
条件构造器用于程序化构建 WHERE 子句。推荐使用 LambdaQueryWrapper 以避免字段名字符串硬编码。
| 方法 | 等价 SQL |
|---|---|
eq(SFunction<T,?> column, Object val) | column = val |
ne(...) | column <> val |
gt(...) | column > val |
ge(...) | column >= val |
lt(...) | column < val |
le(...) | column <= val |
like(...) | column LIKE '%val%' |
between(...) | column BETWEEN val1 AND val2 |
orderByAsc(...) | ORDER BY column ASC |
orderByDesc(...) | ORDER BY column DESC |
多个条件默认以 AND 连接,调用 or() 可切换为 OR。通过 and(Consumer) 可嵌套分组。
分页
分页依赖于 PaginationInnerInterceptor。查询时创建 Page 对象并传入对应方法。
java
Page<Book> page = new Page<>(1, 10); // 第 1 页,每页 10 条
IPage<Book> result = bookService.page(page, null);
result.getTotal(); // 总记录数
result.getRecords(); // 当前页数据BaseMapper.selectPage 返回 Page<T>,IService.page 返回 IPage<T>,行为一致。
基本用法
实体映射
java
@TableName("book")
public class Book {
@TableId(type = IdType.AUTO)
private Long id;
private String title;
private String isbn;
// getter/setter 省略
}@TableName指定表名;不标注时默认将类名转为下划线命名。@TableId声明主键字段,type决定主键生成策略。- 普通字段默认驼峰转下划线,无需额外标注;不一致时使用
@TableField显式指定列名。
Mapper 接口
java
public interface BookMapper extends BaseMapper<Book> {
}继承后即可获得 selectById、insert、updateById、deleteById 等基本方法。相比原生 MyBatis 需要编写接口方法和对应的 XML 映射,这里不需要任何 SQL 语句。
Service 层
java
public interface BookService extends IService<Book> {
}
@Service
public class BookServiceImpl
extends ServiceImpl<BookMapper, Book>
implements BookService {
}ServiceImpl 提供了批量操作和事务支持,如 saveBatch、updateBatchById 等。
条件构造器查询
java
LambdaQueryWrapper<Book> qw = new LambdaQueryWrapper<>();
qw.eq(Book::getIsbn, "978-3-16-148410-0");
Book book = bookMapper.selectOne(qw);等效 SQL 为 SELECT * FROM book WHERE isbn = ?。
分页配置与使用
配置分页插件:
java
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(
new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}调用分页查询:
java
Page<Book> page = new Page<>(1, 10);
Page<Book> result = bookMapper.selectPage(page, null);其中 1 为当前页,10 为每页条数。返回的 Page 对象包含总记录数、总页数以及当前页数据。
示例
示例 1:单表 CRUD
java
// 插入
Book book = new Book();
book.setTitle("MyBatis-Plus Guide");
book.setIsbn("978-3-16-148410-0");
bookMapper.insert(book);
// 按主键查询
Book loaded = bookMapper.selectById(book.getId());
System.out.println(loaded.getTitle());
// 更新
loaded.setTitle("MP Guide");
bookMapper.updateById(loaded);
// 删除
bookMapper.deleteById(loaded.getId());执行后插入的实体主键会被回填,selectById 返回完整对象。整个过程中没有编写任何 SQL 语句。
示例 2:自定义注入全局方法
定义方法类:
java
public class SelectByIsbn extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(
Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = String.format(
"SELECT %s FROM %s WHERE isbn=#{isbn} %s",
sqlSelectColumns(tableInfo, false),
tableInfo.getTableName(),
tableInfo.getLogicDeleteSql(true, true));
SqlSource sqlSource = super.createSqlSource(
configuration, sql, Object.class);
return this.addSelectMappedStatement(
mapperClass, "selectByIsbn", sqlSource,
modelClass, tableInfo);
}
}注册注入器:
java
@Component
public class MyInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(
Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methods = super.getMethodList(mapperClass, tableInfo);
methods.add(new SelectByIsbn());
return methods;
}
}之后所有 Mapper 都会自动拥有 selectByIsbn(String isbn) 方法,无需在每个 Mapper 中重复声明。
注意点
writeReplace策略的延迟失败
如果 JDK 安全策略阻止了writeReplace(例如--illegal-access=deny),并且序列化降级也失败,那么 LambdaQueryWrapper 会在首次执行查询时才抛出异常,而不是在启动阶段。这意味着某个条件查询可能在系统运行较长时间后才暴露问题。自定义方法的一致性要求
自定义AbstractMethod时,如果需要兼容逻辑删除或多租户,必须手动调用tableInfo.getLogicDeleteSql()等方法。这些处理在标准 CRUD 方法中是自动完成的,但自定义方法需要显式保证。缓存不刷新
TableInfo在启动时一次性构建。运行时通过代码切换表名或重命名列不会生效,除非依靠动态表名等下游机制。多插件顺序
分页、多租户、动态表名等插件在不同阶段修改 SQL。如果配置顺序不当,可能产生语法错误的最终 SQL。需理解每个插件的拦截阶段,通常将分页插件放在最后。版本兼容
MP 3.5.x 依赖 MyBatis 3.5.x。版本错配不一定在启动时直接报错,但可能导致NoSuchMethodError等运行时异常。IService 批量操作默认行为
saveBatch的默认实现是逐条 INSERT,并非真正的 JDBC batch。如需使用批量提交,应重写该方法或直接使用 Mapper 的底层批量支持。条件构造器可读性
深层嵌套的and/or条件组合会让 LambdaQueryWrapper 的可读性快速下降,且调试时难以直接获得完整 SQL。对于复杂的动态条件,将 SQL 放回 XML 中维护更合适。分页插件缺失
未配置PaginationInnerInterceptor时,Page对象不会触发物理分页,而是将全部数据加载到内存再截断,高数据量场景容易触发内存溢出。
限制
- MyBatis-Plus 不提供一级缓存、脏检查、级联持久化等功能,与 Hibernate 等全功能 ORM 职责不同。若项目需要这些能力,应直接使用 JPA/Hibernate。
- 条件构造器仅支持方法引用,不支持 Lambda 表达式或匿名内部类。
- 多表查询、ResultMap 的
association/collection、存储过程等场景仍需要通过 MyBatis XML 实现。
应用
引入 MyBatis-Plus 的核心收益是消除单表操作的重复编写,成本是需要团队理解 SQL 注入、插件体系等机制,并明确与原生 MyBatis XML 的使用边界。在选定之前,可以从以下维度进行判断。
何时优先使用 MyBatis-Plus
- 项目中单表 CRUD 操作占比高(例如管理后台、数据字典维护)。
- 字段频繁变更,依赖编译期检查来避免 SQL 字符串错误。
- 团队希望快速生成标准 Mapper/Service/Controller 骨架,缩短微服务或初期模块的开发周期。
- 需要逻辑删除、多租户、动态表名等通用功能,且不希望重复实现。
在这些场景下,MP 可以显著减少模板代码量,同时借助代码生成器保持一致的代码风格。
何时仅使用原生 MyBatis
- 项目涉及大量复杂的多表关联查询、存储过程调用,或对 SQL 的精细调整要求很高。
- 已有大量 XML 映射文件且维护良好,引入 MP 会增加并行抽象层的维护负担。
- 团队规模小,或对 MyBatis 机制的扩展性需求不高,不希望引入额外的学习曲线。
如果项目已经在 MyBatis XML 上形成了成熟的查询封装,且没有大量重复的单表编写工作,引入 MP 可能不会带来明显收益,反而增加了需要理解的概念。
混合使用的边界
当项目中同时存在 MP 和 MyBatis XML 时,需要明确边界:
- 单表操作(包含以单表为主的简单查询)使用
BaseMapper或IService提供的方法。 - 多表关联、动态条件复杂到条件构造器难以表达、存储过程调用则使用 MyBatis XML。
- 如果在同类查询上同时维护了 MP 的条件构造器和 XML,往往意味着边界不清晰,应在开发规范中约定清楚。
混合使用时,分页、多租户等插件同样作用于 XML 中的 SQL,因此要保持插件配置的统一。
代码生成器的使用策略
MP 的代码生成器可以大幅减少初期的骨架编写工作,但生成的代码需要跟随业务演进。如果数据库表结构频繁变动,生成器输出的代码与手写代码混杂在一起,容易带来维护上的混乱。一种做法是将生成代码与手写代码分层存放(例如 gen 包),生成代码不手动修改,定期按新表结构重新生成。
