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 就是字段映射错了;看到分页无效,去检查分页插件配置。
相关推荐
垚垚学技术_聚焦云原生8 小时前
K8s Pod 完整生命周期详解
后端
JavaGuide8 小时前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
不一样的少年_8 小时前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
水深火乐9 小时前
安全地生成验证码、密码和 Token
后端
小强19889 小时前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐9 小时前
interface和any
后端
步行cgn9 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆9 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端
H_Peak9 小时前
Python运算符与流程控制:if条件判断
后端
H_Peak9 小时前
Python字符串操作:从基础到正则入门
后端