java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis-Plus代码实例

MyBatis-Plus站在巨人的肩膀上更进一步

作者:C137的本贾尼

MyBatis-Plus简化开发,提供BaseMapper通用CRUD、条件构造器QueryWrapper与LambdaQueryWrapper,支持SpringBoot3.x,实现分页、逻辑删除、自动填充等进阶功能,这篇文章给大家介绍MyBatis-Plus站在巨人的肩膀上更进一步,感兴趣的朋友一起看看吧

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

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

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

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

学习目标

正文

一、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 方法

@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 不需要写任何方法,就已经有了 insert、selectById、updateById、deleteById 等
}

这些方法是怎么来的?

MP 在启动时,会扫描所有继承了 BaseMapper 的接口,为每个接口生成对应的代理类。MP 在 BaseMapper 中预定义了所有通用方法的 SQL 模板——比如 selectById 的模板大致是:

<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

三、条件构造器体系:QueryWrappervsLambdaQueryWrapper

如果说 BaseMapper 解决的是“标准 CRUD”的问题,那条件构造器解决的就是“动态查询条件”的问题。

MP 提供了两类条件构造器:

QueryWrapper:使用字符串表示字段名。

QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("name", "张三")
       .gt("age", 18)
       .like("email", "test");
List<User> users = userMapper.selectList(wrapper);

LambdaQueryWrapper:使用 Lambda 表达式引用字段。

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:指定实体类对应的数据库表名。

@TableName("t_user")
public class User {
    // ...
}

如果实体类名和表名一致(比如 User 对应 user 表),这个注解可以省略。MP 默认会使用类名的下划线形式作为表名(Useruser)。

@TableId:指定主键字段。

@TableId(value = "user_id", type = IdType.AUTO)
private Integer id;

如果数据库主键列名就是 id,且你想用数据库自增,这个注解也可以省略——MP 会默认把名为 id 的字段当作主键。

@TableField:指定普通字段的映射关系。

@TableField("user_name")
private String username;

@TableField(exist = false)
private String extraInfo;  // 这个字段在数据库中不存在

@TableField 的常见用法:

五、进阶功能:分页、逻辑删除、自动填充

这三个功能是 MP 在企业级项目中使用频率最高的“增强特性”。

分页插件

MP 的分页插件 PaginationInnerInterceptor 提供了强大的分页能力,支持多种数据库。

配置方式(Spring Boot 3.x):

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

使用方式

// 创建分页对象:第 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 的逻辑删除功能会在执行数据库操作时自动处理逻辑删除字段:

配置方式

application.yml 中配置全局逻辑删除属性:

mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted   # 逻辑删除字段名
      logic-delete-value: 1         # 已删除的值
      logic-not-delete-value: 0     # 未删除的值

或者在实体类字段上使用 @TableLogic 注解:

@TableLogic
private Integer deleted;

默认情况下,逻辑未删除值为 0,已删除值为 1。你也可以通过注解的 valuedelval 属性自定义。

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

自动填充

在业务开发中,create_timeupdate_timecreate_by 这类字段几乎每张表都有。每次插入或更新时手动设置,既繁琐又容易遗漏。

MP 的自动填充功能可以帮你自动处理这些字段。

第一步:在实体类中标记需要自动填充的字段

@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

FieldFill 的几种策略:

第二步:实现 MetaObjectHandler 接口:

@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) :

<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.16</version>
</dependency>

实体类

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 接口

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 层使用

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)

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 提供的 deleteByIdapplication.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 不会覆盖或干扰

@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)和动态代理机制实现的。这意味着:

但有一点需要注意:MP 对 MyBatis 的版本有明确的要求,升级 MyBatis 主版本时需要确认 MP 是否已经适配。MP 通常会紧跟 MyBatis 的版本更新,但会有一定的延迟。

思考与延伸

参考与延伸阅读

到此这篇关于MyBatis-Plus站在巨人的肩膀上更进一步的文章就介绍到这了,更多相关MyBatis-Plus代码实例内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文