技术简介
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 Web、H2 Database、Lombok),然后把上面的文件内容替换进去 - 运行 :在项目根目录执行
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 接口生成一个实现类,该实现类里拦截了你调用的selectList、insert等方法 - 它从实体类的注解上拿到表名、列名,从传入的条件构造器(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);
对应 SQL :UPDATE user SET age=30,name='张三丰' WHERE name='张三'。避免先查再改的两次操作。
进阶与避坑
1. 字段映射失效的经典坑
你遇到 Unknown column 'userName',大概率是因为实体字段 userName 默认映射为 user_name,但数据库列名就是 userName。解决:加 @TableField("userName") 或在 application.yml 设 mybatis-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 全加载到内存,用 BaseMapper 的 selectList(resultContext -> ...) 流式处理或配合 MyBatis 的游标。
java
userMapper.selectList(Wrappers.emptyWrapper(), resultContext -> {
User u = resultContext.getResultObject();
// 逐条处理,内存友好
});
下一步
- 官方文档 :baomidou.com 里面有完整的功能列表、配置参考、常见问题解答。遇到某个注解不会用,直接搜索。
- 遇到报错 :先检查控制台打印的实际 SQL(在
application.yml加mybatis-plus.configuration.log-impl: org.apache.ibatis.logging.stdout.StdOutImpl开启 SQL 日志),看看生成的 SQL 是不是你预期的那样。90% 的问题通过观察 SQL 就能定位。 - 独立解决问题 :MyBatis-Plus 的错误信息通常很直白,看到
org.apache.ibatis.binding.BindingException多半是 Mapper 没被扫描到;看到Unknown column就是字段映射错了;看到分页无效,去检查分页插件配置。