第09篇 · MyBatis-Plus:站在巨人的肩膀上更进一步

如果你完整跟完了前两篇,手动写过 MyBatis 的 XML 映射文件,也配置过复杂的 resultMap,那你应该已经感受到了 MyBatis 的灵活和强大。

但你可能也会偶尔冒出这样一个念头:我只是想查一条数据,为什么要写 XML?我只是想做个简单的分页,为什么要自己拼 LIMIT?

MyBatis-Plus(以下简称 MP)的出现,就是为了回答这个问题。它的官方定位是 "只做增强不做改变" ------不改变 MyBatis 的任何现有行为,只在它上面加一层"便利层"。

这一篇,我们把 MP 的核心功能过一遍,看看它到底"增强"了什么。

学习目标

  • 理解 MyBatis-Plus 的定位------只做增强不做改变
  • 掌握 BaseMapper 提供的通用 CRUD 方法(insertselectByIdupdateByIddeleteById 等)
  • 掌握条件构造器(QueryWrapperUpdateWrapperLambdaQueryWrapper)的使用
  • 理解 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 默认会使用类名的下划线形式作为表名(Useruser)。

@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。你也可以通过注解的 valuedelval 属性自定义。

注意事项 :MP 的逻辑删除会自动在查询和更新中过滤已删除数据。但如果业务中有"需要查询已删除数据"的场景,需要绕过这个过滤------可以自己写 SQL,或者在查询时显式指定 ew.apply("deleted = 1")

自动填充

在业务开发中,create_timeupdate_timecreate_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 中已经可以调用 insertselectByIdupdateByIddeleteById 等方法。这些方法全部来自 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
自动填充不生效,createTimeupdateTime 始终为 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 的 IServiceBaseMapper 有什么区别?

BaseMapper 是 Mapper 层的增强,提供的是数据访问方法(CRUD)。IService 是 Service 层的增强,在 BaseMapper 的基础上提供了更多批量操作 (如 saveBatchupdateBatchById)和链式调用 (如 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 的版本更新,但会有一定的延迟。

思考与延伸

  1. 动手对比 :选一个你之前用原生 MyBatis 写过的单表 CRUD 功能,用 MP 的 BaseMapper 重写一遍。对比两种方式的代码行数和开发时间。

  2. 思考题 :MP 的 LambdaQueryWrapper 使用 Lambda 表达式引用 User::getName。这个语法在 Java 8 中是如何实现的?User::getName 本质上是什么类型?提示:SerializedLambda

  3. 延伸阅读: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