欢迎光临
我们一直在努力

《MyBatis》第8章 MyBatis-Plus 增强框架

第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 的核心区别
对比维度MyBatisMyBatis-Plus
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 添加到注入列表,从而实现扩展。

    赞(0)
    未经允许不得转载:171主机测评 » 《MyBatis》第8章 MyBatis-Plus 增强框架
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址