MyBatis-Plus

技术简介

MyBatis-Plus(简称 MP)是一个 MyBatis 的增强工具包。它在你熟悉的 MyBatis 之上做了增强,但不改变任何原有的东西------你之前怎么写 MyBatis,现在依然可以那么写,只是多了一个更省事的选项。

它核心解决一件事:消灭简单又重复的 CRUD(增删改查)代码。比如,你原本要为每个表写"根据 ID 查一条记录""查询所有记录""插入一条记录"这些几乎一模一样的 SQL 和 Mapper 方法。用 MP 后,直接继承一个接口,这些方法就已经在那了,一行不用写。它还能帮你处理分页、逻辑删除、自动填充时间戳等高频需求。

一句话:让你只写真正有业务价值的 SQL,剩下的全交给 MP。

快速开始(5 分钟上手)

我们用一个 Spring Boot + H2 内存数据库 的项目来演示。你不用装任何外部数据库,复制代码就能跑。

最简示例

完整项目结构如下(只需 4 个文件):

css 复制代码
src/main/java/com/example/demo/
├── DemoApplication.java
├── entity/
│   └── User.java
└── mapper/
    └── UserMapper.java
src/main/resources/
└── application.yml

1. pom.xml 添加依赖(在 <dependencies> 里)

xml 复制代码
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.5</version>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

2. application.yml

yaml 复制代码
spring:
  datasource:
    url: jdbc:h2:mem:testdb
    driver-class-name: org.h2.Driver
    username: sa
    password:
  h2:
    console:
      enabled: true   # 可以通过浏览器看数据库

3. User.java

java 复制代码
package com.example.demo.entity;

import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;

@Data
@TableName("user")        // 告诉 MP 这张表叫 user
public class User {
    @TableId              // 标记这是主键,默认雪花算法生成 ID
    private Long id;
    private String name;
    private Integer age;
}

4. UserMapper.java

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 后,CRUD 方法已经全部拥有
    // 不需要写任何代码,就能用 userMapper.selectList() 等
}

5. DemoApplication.java(启动 + 测试)

java 复制代码
package com.example.demo;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.example.demo.entity.User;
import com.example.demo.mapper.UserMapper;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.Statement;
import java.util.List;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    // 应用一启动就执行:建表 → 插数据 → 查询打印
    @Bean
    CommandLineRunner runner(UserMapper userMapper, DataSource dataSource) {
        return args -> {
            // 用 JDBC 建表和插入测试数据
            try (Connection conn = dataSource.getConnection();
                 Statement stmt = conn.createStatement()) {
                stmt.execute("CREATE TABLE user (id BIGINT PRIMARY KEY, name VARCHAR(50), age INT)");
                stmt.execute("INSERT INTO user VALUES (1, '张三', 22)");
                stmt.execute("INSERT INTO user VALUES (2, '李四', 28)");
            }

            // 用 MP 查询所有用户
            List<User> users = userMapper.selectList(Wrappers.emptyWrapper());
            users.forEach(u -> System.out.println(u.getName() + " - " + u.getAge()));
        };
    }
}

安装与运行

  • 环境准备:JDK 17+(或 8+),Maven 3.6+,任意 IDE 或命令行
  • 创建项目 :用 Spring Initializr 创建一个 Spring Boot 项目(依赖选 Spring WebH2 DatabaseLombok),然后把上面的文件内容替换进去
  • 运行 :在项目根目录执行 mvn spring-boot:run(Windows / macOS / Linux 通用)
  • 期望看到 :控制台输出 张三 - 22李四 - 28,且程序正常退出(因为是命令行运行,跑完就停了)

看到效果

看到两条用户信息打印出来,就代表 MyBatis-Plus 已经在你的项目中成功运行起来了。你一行 Mapper XML 没写、一句 SQL 没配,就完成了查询。

必会基础

1. 实体与表映射 ------ 让 MP 认识你的 Java 对象

一句话定义:通过几个简单的注解,告诉 MP 你的 Java 类对应数据库哪张表、哪个字段是主键、哪个字段和列名不一样。

java 复制代码
@Data
@TableName("sys_user")               // 如果表名和类名不一致,必须指定
public class User {
    @TableId(type = IdType.AUTO)     // 主键自增(默认用雪花算法生成)
    private Long userId;             // 字段名和列名不一样时要标记
    @TableField("user_name")
    private String name;
    @TableField(exist = false)       // 这个属性不在数据库里,忽略
    private String extra;
}

典型场景

  • 你的 Java 类属性名是驼峰,数据库列名带下划线时,MP 会自动转化,绝大多数情况不需要加任何注解
  • 主键不是 id 或生成策略特殊时,用 @TableId
  • 某个字段数据库没有,用 exist = false

边界提醒 :不要在一个类里混合 @TableId 的多种策略却不理解其行为,比如有些字段用 AUTO 有些用 ASSIGN_ID,会导致插入时主键值不符合预期。

2. BaseMapper ------ 一行不写的增删改查

一句话定义 :只要你的 Mapper 接口继承 BaseMapper<实体类>,立刻拥有 17 个常用 CRUD 方法,不用写任何实现。

java 复制代码
@Mapper
public interface UserMapper extends BaseMapper<User> { }

// 使用
@Autowired
private UserMapper userMapper;

// 查所有
List<User> list = userMapper.selectList(null);
// 根据 ID 查
User u = userMapper.selectById(1L);
// 插入(自动回填主键)
userMapper.insert(new User().setName("王五"));
// 根据 ID 更新
userMapper.updateById(new User().setId(1L).setAge(30));
// 根据 ID 删除
userMapper.deleteById(2L);

典型场景:单表的简单增删改查,不用写一行 SQL。

边界提醒:多表关联、复杂统计、动态条件极多时别硬用 BaseMapper 组合,那时你应该用自定义 XML 或注解写原生 SQL,MP 不拦着你。

3. 条件构造器 Wrapper ------ 把 SQL 的 WHERE 部分写成链式调用

一句话定义 :用纯 Java 代码拼装查询条件,替代你在 XML 里写 <if> 标签的繁琐,让动态查询更流畅。

java 复制代码
// 查年龄大于 20 且名字带"张"的用户,按年龄倒序
List<User> list = userMapper.selectList(
    new LambdaQueryWrapper<User>()
        .gt(User::getAge, 20)            // age > 20
        .likeRight(User::getName, "张")  // name like '张%'
        .orderByDesc(User::getAge)       // ORDER BY age DESC
);

典型场景

  • 多条件组合查询,条件个数和种类由前端参数决定
  • 不想在 XML 里写一堆 <if test> 标签

边界提醒 :函数式写法里的 User::getAge 是 Getter 引用,MP 会转成对应数据库列 age。如果你手写了列名,容易打错还不报编译错误,推荐全部用 Lambda 写法 ,避免字符串硬编码。你这样用会出问题:eq("agge", 20) 拼错列名,只在运行时爆出异常。

4. 分页查询

一句话定义:加一个插件,传一个分页对象,MP 自动帮你拼装分页 SQL 并返回总数和分页数据。

先添加分页插件配置(任意配置类):

java 复制代码
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
        return interceptor;
    }
}

然后使用:

java 复制代码
Page<User> page = new Page<>(1, 5);   // 第1页,每页5条
Page<User> result = userMapper.selectPage(page, 
    new LambdaQueryWrapper<User>().gt(User::getAge, 18));
System.out.println("总记录数:" + result.getTotal());
System.out.println("当前页数据:" + result.getRecords());

典型场景:任何需要分页展示的列表接口。

边界提醒 :分页插件默认会对所有 Mapper 方法拦截并尝试分页,如果你某个查询不需要分页但用了同样的 Mapper 方法,注意它不会受影响,只有你主动传 Page 参数时才会分页。另外记得不要忘记配置插件,否则分页失效,查出来的是全量数据。

核心原理

你只需要掌握一个核心机制:动态代理 + SQL 自动生成

  • 当你继承 BaseMapper<User> 时,MP 在启动时通过动态代理为你的 Mapper 接口生成一个实现类,该实现类里拦截了你调用的 selectListinsert 等方法
  • 它从实体类的注解上拿到表名、列名,从传入的条件构造器(Wrapper)拿到 WHERE 部分,然后用一套 SQL 模板拼接出完整 SQL
  • 最后交给 MyBatis 执行,返回结果

理解这一点就够了:你调用的那些 CRUD 方法不是直接操作数据库,而是 MP 在内存里"现写" SQL 再执行。这就解释了为什么 Wrapper 里的条件最后能变成 WHERE 子句,为什么分页要加插件才能拦截拼接 LIMIT。

实战模式

1. 逻辑删除 ------ 删数据变成改标记

需求:用户点"删除",你不真删数据,而是把 deleted 字段改成 1。

java 复制代码
// 实体类
@TableLogic
private Integer deleted;  // 0未删除,1已删除

// application.yml
mybatis-plus:
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1   # 删除后的值
      logic-not-delete-value: 0

现在调用 userMapper.deleteById(1L) 就会变成 UPDATE user SET deleted=1 WHERE id=1,所有查询自动带上 AND deleted=0

提醒 :唯一索引列小心,被逻辑删除的记录可能占用唯一值,导致新插入冲突。你这样用会出问题:需求要求逻辑删除后可恢复,但恢复时改了 deleted=0 却和其他未删除的唯一键冲突。

2. 自动填充 ------ 创建时间、更新时间不用手动设

java 复制代码
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
    }
    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
    }
}

// 实体类字段上加注解
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;

插入或更新实体时,这些字段会自动被赋予当前时间,不用每次 set。

3. 代码生成器 ------ 快速生成实体、Mapper、Service

适合新表建完后迅速铺好基础代码。使用官方推荐的 MybatisPlusGenerator 或直接 IDEA 插件(MyBatisX 插件),根据数据库表一键生成实体、Mapper、Service、Controller。

手写方式(简要):

java 复制代码
FastAutoGenerator.create("jdbc:mysql://localhost:3306/db", "root", "pwd")
    .globalConfig(builder -> builder.author("YourName").outputDir("输出路径"))
    .packageConfig(builder -> builder.parent("com.example"))
    .strategyConfig(builder -> builder.addInclude("user")) // 表名
    .execute();

生成后你只需改业务逻辑,不用再重复建实体和 Mapper。

4. 条件更新 ------ 只更新符合条件的某几列

java 复制代码
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getName, "张三")
       .set(User::getAge, 30)          // 只改 age
       .set(User::getName, "张三丰");  // 同时改 name
userMapper.update(null, wrapper);

对应 SQLUPDATE user SET age=30,name='张三丰' WHERE name='张三'。避免先查再改的两次操作。

进阶与避坑

1. 字段映射失效的经典坑

你遇到 Unknown column 'userName',大概率是因为实体字段 userName 默认映射为 user_name,但数据库列名就是 userName。解决:加 @TableField("userName") 或在 application.ymlmybatis-plus.configuration.map-underscore-to-camel-case: false

2. 主键策略选择

  • 分布式系统:用默认的 ASSIGN_ID(雪花算法),生成 Long 型 ID
  • 单机自增:@TableId(type = IdType.AUTO),但依赖数据库自增主键
  • 你这样用会出问题:用自增主键却在插入时手动 set 了一个 id,结果插入报主键冲突,或数据库忽略了你的值

3. 批量操作注意

userMapper.insertBatch(list) 实际上是把多条 INSERT 拼成一条 SQL 执行,数据量过大可能导致 SQL 过长或一次插入太多。建议每 1000 条分批。

4. 乐观锁

解决并发更新时"后提交覆盖前提交"的问题。只需实体加 @Version 注解,并配置乐观锁插件。

java 复制代码
@Version
private Integer version;

更新时 MP 自动带上 WHERE version = 原值,并把 version+1。如果更新行数为 0 表示版本已变,可自行重试或抛异常。

5. 流式查询(大数据量)

查询几十万条数据时不要用 selectList 全加载到内存,用 BaseMapperselectList(resultContext -> ...) 流式处理或配合 MyBatis 的游标。

java 复制代码
userMapper.selectList(Wrappers.emptyWrapper(), resultContext -> {
    User u = resultContext.getResultObject();
    // 逐条处理,内存友好
});

下一步

  • 官方文档baomidou.com 里面有完整的功能列表、配置参考、常见问题解答。遇到某个注解不会用,直接搜索。
  • 遇到报错 :先检查控制台打印的实际 SQL(在 application.ymlmybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 开启 SQL 日志),看看生成的 SQL 是不是你预期的那样。90% 的问题通过观察 SQL 就能定位。
  • 独立解决问题 :MyBatis-Plus 的错误信息通常很直白,看到 org.apache.ibatis.binding.BindingException 多半是 Mapper 没被扫描到;看到 Unknown column 就是字段映射错了;看到分页无效,去检查分页插件配置。
相关推荐
doiito1 小时前
微服务资源优化新思路:Sablier + Podman 实现按需启动与自动休眠
后端·微服务·性能优化
接着奏乐接着舞。1 小时前
【2026年7月最新】69道RAG面试题
前端·人工智能·后端·aigc·embedding·rag
edwarddamon1 小时前
Spring Cloud Gateway 全链路 traceId 传递方案
后端
神奇小汤圆1 小时前
我们如何在 Elasticsearch 上构建一个持久 agent 记忆层,实现 0.89 召回率和零租户泄漏
后端
掘金一周2 小时前
裁员了,公司很爽快,赔偿一分没少 | 沸点周刊 7.8
前端·人工智能·后端
神奇小汤圆2 小时前
MySQL 5.7 频繁被 Systemd 杀死:一次惊心动魄的故障排查之旅
后端
行思理2 小时前
fastadmin 新手部分功能点
后端·php
Fanta丶2 小时前
5.Activiti7工作流启动和查询流程、流程任务处理
后端
꒰ঌ 安卓开发໒꒱2 小时前
.NET CAP入门到入土
后端·c#·.net