第8章 MyBatis-Plus 增强框架——从 CRUD 到 Lambda 查询
8.1 MyBatis-Plus 概述
8.1.1 定位:对 MyBatis 的增强,而非替代
MyBatis-Plus 的官方定位是 MyBatis 的增强工具,其核心设计理念是 "只做增强,不做改变"。这意味着:
-
MP 完全兼容 MyBatis 的所有特性,原有的 MyBatis 配置、Mapper XML 文件、自定义 SQL 都可以继续使用。
-
MP 在 MyBatis 的基础上增加了通用 CRUD、条件构造器、代码生成器等实用功能,旨在简化开发、提高效率。

8.1.2 与 MyBatis 的核心区别
| SQL 编写 | 需手动编写 SQL(XML/注解) | 单表 CRUD 零 SQL,内置通用方法 |
| 开发效率 | 中等,需关注 SQL 细节 | 高,单表操作几乎无需编写 SQL |
| 查询构建 | 手动拼接 SQL 条件 | 条件构造器(Wrapper),链式调用,支持 Lambda |
| 分页功能 | 需手动实现或集成第三方插件 | 内置分页插件,自动生成方言分页 SQL |
| 乐观锁 | 需手动实现 | 内置乐观锁插件,通过 @Version 注解自动实现 |
| 自动填充 | 不支持 | 支持创建时间、更新时间等字段自动填充 |
| 逻辑删除 | 需手动处理 | 内置逻辑删除插件,通过 @TableLogic 注解支持 |
| 代码生成器 | 无 | 内置代码生成器,可根据表结构生成 Entity、Mapper、Service、Controller |
| 学习成本 | 低(专注 SQL) | 中(需学习 MP 的 API 和条件构造器) |
8.2 核心特性深度解析
8.2.1 通用 Mapper:BaseMapper 的秘密
当你的 Mapper 接口继承 BaseMapper<T> 后,立即拥有了数十个常用的 CRUD 方法,无需编写任何 SQL 或 XML。
public interface UserMapper extends BaseMapper<User> {
// 无需任何方法定义,即可使用 insert、deleteById、selectById、selectList 等
}
// 使用示例
User user = userMapper.selectById(1L); // 根据 ID 查询
List<User> users = userMapper.selectList(null); // 查询全部
userMapper.insert(new User("张三", 25)); // 插入
源码揭秘:BaseMapper 中定义的方法是如何变成可执行的 SQL 的?关键在于 SQL 注入器(SqlInjector)。

在 MybatisMapperAnnotationBuilder.parse() 方法中,MP 会判断当前 Mapper 是否是 BaseMapper 的子接口,如果是,则通过 SqlInjector 注入所有通用方法:
// MybatisMapperAnnotationBuilder.parse() 简化版
public void parse() {
// … 解析注解等
if (BaseMapper.class.isAssignableFrom(mapperType)) {
// 获取 SQL 注入器
SqlInjector sqlInjector = GlobalConfigUtils.getSqlInjector(configuration);
// 注入通用方法
sqlInjector.inspectInject(mapperType, configuration);
}
}
SqlInjector 会遍历所有预定义的通用方法(如 insert、deleteById、selectById 等),为每个方法创建 MappedStatement 并注册到 Configuration 中。
8.2.2 条件构造器:Wrapper 体系
条件构造器是 MP 最强大的特性之一,它允许以链式调用的方式构建查询条件,无需手动拼接 SQL。
Wrapper 类层次结构:

QueryWrapper 示例(字符串方式):
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三")
.gt("age", 18)
.like("email", "@gmail.com")
.orderByDesc("create_time");
List<User> users = userMapper.selectList(wrapper);
生成 SQL:
SELECT * FROM user
WHERE name = '张三'
AND age > 18
AND email LIKE '%@gmail.com%'
ORDER BY create_time DESC
LambdaQueryWrapper 示例(类型安全):
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(User::getName, "张三")
.gt(User::getAge, 18)
.like(User::getEmail, "@gmail.com")
.orderByDesc(User::getCreateTime);
List<User> users = userMapper.selectList(lambdaWrapper);
Lambda 表达式的优势:
-
类型安全:字段名通过方法引用 User::getName 获取,编译期即可检查字段是否存在,避免字符串拼写错误。
-
重构友好:如果实体类字段名变更,IDE 可以自动重构所有引用。
源码解析:LambdaQueryWrapper 如何通过 User::getName 获取字段名?
当调用 eq(User::getName, "张三") 时,LambdaQueryWrapper 会将 User::getName 这个 SerializedLambda 解析为对应的数据库字段名。核心原理是利用 Java 8 的 SerializedLambda 机制,从 Lambda 表达式的方法引用中提取出属性名,再根据 MP 的命名策略转换为数据库字段名(如 user_name)。
8.2.3 分页插件:自动物理分页
MP 内置的分页插件通过拦截器机制实现物理分页,避免了内存分页的性能问题。
配置分页插件:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
使用分页:
@GetMapping("/list")
public IPage<User> list(Page<User> page) {
// 只需传入 Page 对象,MP 自动生成分页 SQL
return userService.page(page);
}
源码原理:PaginationInnerInterceptor 实现了 MyBatis 的 Interceptor 接口,拦截 Executor 的 query 方法。它会判断参数中是否有 Page 对象,如果有,则:
先执行一个 COUNT 查询获取总记录数。
改写原 SQL,添加对应数据库的分页语法(如 MySQL 的 LIMIT)。
执行分页查询并封装结果到 Page 对象。
8.2.4 乐观锁插件
乐观锁用于解决并发更新时的数据一致性问题。MP 通过 @Version 注解自动实现乐观锁。
配置乐观锁插件:
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
实体类:
public class User {
@Version
private Integer version;
// 其他字段…
}
使用:当执行更新时,MP 会自动将 version 作为更新条件,并递增版本号。
如果更新行数为 0,说明数据已被其他事务修改,抛出异常。
8.2.5 自动填充功能
自动填充功能允许在插入或更新时自动填充字段值,如创建时间、更新时间等。
实现 MetaObjectHandler:
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", Date.class, new Date());
this.strictInsertFill(metaObject, "updateTime", Date.class, new Date());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
}
}
实体类注解:
public class User {
@TableField(fill = FieldFill.INSERT)
private Date createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
8.3 ⭐SQL 注入器源码分析
SQL 注入器是 MP 的核心组件,负责将 BaseMapper 中的方法注入为可执行的 SQL 语句。
8.3.1 默认注入器:DefaultSqlInjector
DefaultSqlInjector 是 MP 默认的 SQL 注入器实现,它继承自 AbstractSqlInjector,提供了所有通用方法的注入逻辑。
public class DefaultSqlInjector extends AbstractSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
return Stream.of(
new Insert(), // 插入方法
new Delete(), // 删除方法
new DeleteByMap(),
new DeleteById(),
new DeleteBatchByIds(),
new Update(), // 更新方法
new UpdateById(),
new SelectById(), // 查询方法
new SelectBatchByIds(),
new SelectByMap(),
new SelectOne(),
new SelectCount(),
new SelectMaps(),
new SelectMapsPage(),
new SelectObjs(),
new SelectList(),
new SelectPage()
).collect(Collectors.toList());
}
}
每个 AbstractMethod 子类负责定义一个具体的方法,例如 SelectById:
public class SelectById extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = "<script>%s SELECT %s FROM %s WHERE %s=#{%s} %s</script>";
String methodName = "selectById";
SqlSource sqlSource = languageDriver.createSqlSource(configuration,
String.format(sql,
tableInfo.getLogicDeleteSql(true, true), // 逻辑删除条件
tableInfo.getAllSqlSelect(), // 查询字段
tableInfo.getTableName(), // 表名
tableInfo.getKeyColumn(), // 主键列
tableInfo.getKeyProperty(), // 主键属性
tableInfo.getLogicDeleteSql(true, false) // 额外条件
), modelClass);
return this.addSelectMappedStatementForTable(mapperClass, methodName, sqlSource, modelClass, tableInfo);
}
}
8.3.2 自定义 SQL 注入器
如果需要扩展通用方法,可以实现自定义 SQL 注入器。
步骤 1:定义抽象方法类
public class FindByName extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = "<script>SELECT %s FROM %s WHERE name = #{name}</script>";
SqlSource sqlSource = languageDriver.createSqlSource(configuration,
String.format(sql, tableInfo.getAllSqlSelect(), tableInfo.getTableName()), modelClass);
return this.addSelectMappedStatementForTable(mapperClass, "findByName", sqlSource, modelClass, tableInfo);
}
}
步骤 2:自定义注入器
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
methodList.add(new FindByName()); // 添加自定义方法
return methodList;
}
}
步骤 3:配置使用自定义注入器
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() {
return plusProperties -> plusProperties.getGlobalConfig().setSqlInjector(new MySqlInjector());
}
}
8.4 利弊分析与混合使用策略
8.4.1 核心优势
| 开发效率极高 | 单表 CRUD 零 SQL,代码量减少 60% 以上 |
| 功能丰富 | 分页、乐观锁、逻辑删除、自动填充等开箱即用 |
| 条件构造器强大 | 链式调用构建复杂查询,Lambda 表达式类型安全 |
| 代码生成器 | 根据表结构一键生成 Entity、Mapper、Service、Controller |
| 完全兼容 MyBatis | 可与原生 MyBatis 混合使用,平滑迁移 |
8.4.2 局限性
| 复杂 SQL 支持有限 | 多表关联、子查询、复杂统计等仍需手写 SQL |
| 封装过深 | 可能掩盖数据库设计缺陷,自动生成的方法可能诱导开发者忽略性能优化 |
| 学习成本 | 需学习 Wrapper 体系、插件配置等 API |
| 动态 SQL 灵活性不如原生 | 某些复杂动态条件用 Wrapper 表达可能比 XML 更复杂 |
8.4.3 复杂 SQL 场景的处理
当遇到 MP 无法自动生成的复杂 SQL 时,有多种解决方案:
方案 1:在 Mapper 接口中直接使用注解编写 SQL
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT u.*, d.name as dept_name " +
"FROM user u LEFT JOIN dept d ON u.dept_id = d.id " +
"WHERE u.age > #{age} AND d.name = #{deptName}")
List<User> selectUsersWithDept(@Param("age") Integer age, @Param("deptName") String deptName);
}
方案 2:使用原生 MyBatis XML 映射文件
在 application.yml 中配置 XML 文件位置:
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
然后在 resources/mapper 目录下编写 XML 文件:
<mapper namespace="com.xyz.mapper.UserMapper">
<select id="selectComplex" resultType="User">
SELECT u.*, d.name as dept_name
FROM user u
LEFT JOIN dept d ON u.dept_id = d.id
WHERE u.age > #{age}
AND EXISTS (SELECT 1 FROM role r WHERE r.user_id = u.id AND r.type = #{type})
</select>
</mapper>
方案 3:使用 Wrapper 的 apply 方法嵌入 SQL 片段
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.apply("age > (SELECT AVG(age) FROM user)")
.eq(User::getDeptId, 1);
8.5 本章小结
-
MyBatis-Plus 是对 MyBatis 的增强而非替代,完全兼容原生 MyBatis。
-
核心特性包括通用 Mapper、条件构造器(Wrapper)、分页插件、乐观锁、自动填充等,极大地提升了开发效率。
-
SQL 注入器是 MP 的核心组件,负责将 BaseMapper 的方法注入为可执行的 SQL 语句。
-
条件构造器支持链式调用和 Lambda 表达式,提供了类型安全的查询构建方式。
-
局限性:复杂 SQL 支持有限,需回归原生 MyBatis。
-
最佳实践:单表 CRUD 用 MP,复杂查询用原生 MyBatis,两者混合使用可兼顾效率与灵活性。
⭐面试题
1. MyBatis-Plus 和 MyBatis 的核心区别是什么?在什么场景下你会选择 MyBatis-Plus?
考察点:对两个框架定位的理解,以及根据场景选择合适技术的能力。
完整答复:
核心区别:
-
定位:MyBatis 是一个半自动 ORM 框架,需要手动编写 SQL;MyBatis-Plus 是 MyBatis 的增强工具,在 MyBatis 基础上增加了通用 CRUD、条件构造器等功能。
-
SQL 编写:MyBatis 所有 SQL 都需要手动编写;MyBatis-Plus 单表 CRUD 无需编写 SQL,内置通用方法。
-
查询构建:MyBatis 需手动拼接 SQL 条件;MyBatis-Plus 提供条件构造器(Wrapper),支持链式调用和 Lambda 表达式。
-
功能丰富度:MyBatis-Plus 内置分页、乐观锁、自动填充、逻辑删除、代码生成器等开箱即用的功能。
选择 MyBatis-Plus 的场景:
-
快速开发原型系统或中小型项目,单表 CRUD 操作占比高。
-
后台管理系统,大部分操作是简单的单表增删改查。
-
团队追求开发效率,希望减少重复编写 SQL 的工作量。
-
需要分页、乐观锁等通用功能,不想自己实现或集成第三方插件。
选择原生 MyBatis 的场景:
-
复杂查询需求多,如多表关联、子查询、报表统计。
-
需要精细控制 SQL 执行计划,进行深度性能优化。
-
遗留系统改造,已有大量手写 SQL 资产。
2. MyBatis-Plus 的条件构造器(Wrapper)是如何工作的?LambdaQueryWrapper 相比 QueryWrapper 有什么优势?
考察点:对 Wrapper 体系的理解,以及类型安全的认识。
完整答复:
Wrapper 的工作原理:
-
Wrapper 是一个用于构建 SQL WHERE 条件的链式调用工具。
-
当调用 eq("name", "张三") 等方法时,Wrapper 内部会记录条件表达式,最终在执行查询时将这些条件拼接成完整的 SQL 语句。
-
核心类 AbstractWrapper 提供了所有条件方法(eq、gt、like 等),QueryWrapper 和 UpdateWrapper 继承自它。
-
在 selectList 等方法执行时,MP 会从 Wrapper 中提取条件片段,通过 SqlSource 生成最终的 SQL。
LambdaQueryWrapper 的优势:
类型安全:通过 User::getName 方法引用字段,编译期即可检查字段是否存在,避免字符串拼写错误导致的运行时异常。
重构友好:如果实体类字段名变更,IDE 可以自动重构所有 Lambda 表达式引用,而字符串方式需要手动查找替换。
代码可读性:Lambda 表达式使代码更加直观,IDE 可以提供更好的代码补全和导航。
示例对比:
// QueryWrapper – 字符串方式,容易写错
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("user_name", "张三"); // 如果字段名是 username,此处错误只能在运行时发现
// LambdaQueryWrapper – 类型安全
LambdaQueryWrapper<User> lambdaWrapper = new LambdaQueryWrapper<>();
lambdaWrapper.eq(User::getUserName, "张三"); // 编译期检查
3. MyBatis-Plus 的 SQL 注入器(SqlInjector)的作用是什么?如何自定义一个通用方法?
考察点:对 MP 核心扩展机制的理解。
完整答复:
SQL 注入器的作用:
SQL 注入器是 MP 的核心组件,负责将 BaseMapper 中定义的通用方法(如 insert、selectById 等)注入为 MyBatis 可执行的 MappedStatement。它通过遍历预定义的 AbstractMethod 列表,为每个方法生成对应的 SQL 语句,并注册到 Configuration 中,使得这些方法可以在 Mapper 接口中直接使用。
自定义通用方法的步骤:
步骤 1:定义 AbstractMethod 子类
public class FindByEmail extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass,
Class<?> modelClass,
TableInfo tableInfo) {
// 构建 SQL 模板
String sql = "<script>SELECT %s FROM %s WHERE email = #{email}</script>";
String selectSql = String.format(sql,
tableInfo.getAllSqlSelect(),
tableInfo.getTableName());
// 创建 SqlSource
SqlSource sqlSource = languageDriver.createSqlSource(configuration,
selectSql, modelClass);
// 注册 MappedStatement
return this.addSelectMappedStatementForTable(mapperClass,
"findByEmail", sqlSource, modelClass, tableInfo);
}
}
步骤 2:创建自定义 SQL 注入器
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 获取默认方法列表
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
// 添加自定义方法
methodList.add(new FindByEmail());
return methodList;
}
}
步骤 3:配置使用自定义注入器
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusPropertiesCustomizer plusPropertiesCustomizer() {
return plusProperties -> plusProperties.getGlobalConfig()
.setSqlInjector(new MySqlInjector());
}
}
步骤 4:在 Mapper 中使用
public interface UserMapper extends BaseMapper<User> {
// 无需实现,SQL 注入器会自动提供
User findByEmail(@Param("email") String email);
}
原理:当 MP 启动扫描 Mapper 时,会通过 MybatisMapperAnnotationBuilder 检查 Mapper 是否继承自 BaseMapper,如果是,则调用 SqlInjector.inspectInject() 注入所有通用方法。自定义方法通过重写 getMethodList 添加到注入列表,从而实现扩展。



