跳转至

MyBatis-Plus

约 2099 个字 173 行代码 预计阅读时间 9 分钟

依赖引入

XML
1
2
3
4
5
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
    <version>3.5.14</version>
</dependency>

数据准备

SQL
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for book_info
-- ----------------------------
DROP TABLE IF EXISTS `book_info`;
CREATE TABLE `book_info`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `book_name` varchar(127) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `author` varchar(127) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `count` int NOT NULL,
  `price` decimal(7, 2) NOT NULL,
  `publish` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL,
  `status` tinyint NULL DEFAULT 1 COMMENT '0-⽆效, 1-正常, 2-不允许借阅',
  `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 32 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of book_info
-- ----------------------------
INSERT INTO `book_info` VALUES (1, '活着', '余华', 29, 22.00, '北京文艺出版社', 1, '2025-09-14 19:38:41', '2025-09-15 19:50:52');
INSERT INTO `book_info` VALUES (2, '平凡的世界', '路遥', 5, 98.56, '北京十月文艺出版社', 1, '2025-09-14 19:38:41', '2025-09-15 19:52:02');
INSERT INTO `book_info` VALUES (3, '三体', '刘慈欣', 9, 102.67, '重庆出版社', 1, '2025-09-14 19:38:41', '2025-09-15 19:52:09');
INSERT INTO `book_info` VALUES (4, '金字塔原理', '麦肯锡', 16, 178.00, '民主与建设出版社', 1, '2025-09-14 19:38:41', '2025-09-15 19:54:32');
INSERT INTO `book_info` VALUES (5, '云边有个小卖部', '张嘉佳', 1, 54.00, '湖南文艺出版社', 1, '2025-09-14 21:35:55', '2025-09-14 21:41:07');
INSERT INTO `book_info` VALUES (6, '被讨厌的勇气:“自我启发之父”阿德勒的哲学课', '岸见一郎', 1, 58.00, '机械工业出版社', 1, '2025-09-15 18:15:04', '2025-09-15 18:15:04');
INSERT INTO `book_info` VALUES (7, '流浪地球', '刘慈欣', 1, 87.00, '四川科学技术出版社', 1, '2025-09-15 18:16:33', '2025-09-15 19:55:21');
INSERT INTO `book_info` VALUES (8, '图书2', '作者2', 29, 22.00, '出版社2', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (9, '图书3', '作者2', 29, 22.00, '出版社3', 0, '2025-09-15 21:26:39', '2025-09-20 17:38:50');
INSERT INTO `book_info` VALUES (10, '图书4', '作者2', 29, 22.00, '出版社1', 0, '2025-09-15 21:26:39', '2025-09-20 14:50:07');
INSERT INTO `book_info` VALUES (11, '图书5', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (12, '图书6', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (13, '图书7', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (14, '图书8', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (15, '图书9', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (16, '图书10', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (17, '图书11', '作者2', 29, 22.00, '出版社1', 0, '2025-09-15 21:26:39', '2025-09-20 14:52:12');
INSERT INTO `book_info` VALUES (18, '图书12', '作者2', 29, 22.00, '出版社1', 0, '2025-09-15 21:26:39', '2025-09-20 14:52:12');
INSERT INTO `book_info` VALUES (19, '图书13', '作者2', 29, 22.00, '出版社1', 0, '2025-09-15 21:26:39', '2025-09-20 14:52:12');
INSERT INTO `book_info` VALUES (20, '图书14', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (21, '图书15', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (22, '图书16', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (23, '图书17', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (24, '图书18', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (25, '图书19', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (26, '图书20', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');
INSERT INTO `book_info` VALUES (27, '图书21', '作者2', 29, 22.00, '出版社1', 1, '2025-09-15 21:26:39', '2025-09-15 21:26:39');


SET FOREIGN_KEY_CHECKS = 1;

自动填空字段

假设数据库已经配置了下面两个字段:

SQL
1
2
create_time DATETIME
update_time DATETIME

要使用MyBatis-Plus的自动填充字段功能,首先需要在实体类上使用@TableField注解并填入需要的枚举值:

Java
1
2
3
4
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.UPDATE)
private LocalDateTime updateTime;

接着创建一个配置类实现MetaObjectHandler,在该配置类中重写insertFillupdateFill方法:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Component
public class MyBatisPlusTimeMetaObjectHandler 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());
    }
}

此时再插入/更新数据时可以不用显式指定时间,MyBatis-Plus会自动处理时间

需要注意的是,如果在更新数据时使用的是update(wrapper updateWrapper/LambdaUpdateWrapper)的,自动填充字段将不会触发,更推荐使用下面两种方案:

  1. 使用带实体参数和UpdateWrapper的重载版本:update(T entity, updateWrapper/LambdaUpdateWrapper),其中entity可以传一个临时对象,例如new User()
  2. 手动构建携带新值对象+使用updateById方法
为什么自动填充在使用update(wrapper)版本会失效,但是update(T entity, wrapper)版本又可以?

MyBatis-Plus的自动填充逻辑在底层需要拿到实体类对应的TableInfo,从而判断哪些字段标注了@TableField(fill = ...)。核心源码逻辑大致如下(com.baomidou.mybatisplus.core.toolkit.ReflectionKit / TableInfoHelper):

Java
1
2
3
4
5
6
TableInfo tableInfo = TableInfoHelper.getTableInfo(parameterObject.getClass());
if (null == tableInfo) {
    // 没有 TableInfo,直接返回,不执行填充
    return parameterObject;
}
// 根据 TableInfo 找到需要填充的字段,调用 metaObjectHandler.updateFill(...)

其中的parameterObject它是Mapper方法的第一个参数对象,不同的调用方式,parameterObject指向如下表:

调用方式 parameterObject实际指向
insert(sysUser) sysUser这个实体对象
updateById(sysUser) sysUser这个实体对象
update(sysUser, wrapper) sysUser这个实体对象
update(null, wrapper) 第一个参数为null,MyBatis-Plus会把wrapper当作 parameterObject
update(wrapper) wrapper

对于parameterObject.getClass()来说,拿到parameterObject的运行时类型,对应表如下:

调用方式 getClass() 结果
insert(sysUser) SysUser.class
updateById(sysUser) SysUser.class
update(sysUser, wrapper) SysUser.class
update(null, wrapper) LambdaUpdateWrapper.class / UpdateWrapper.class
update(wrapper) LambdaUpdateWrapper.class / UpdateWrapper.class

TableInfoHelper是MyBatis-Plus的工具类,内部维护了一张类 → 表信息的映射缓存,调用getTableInfo(User.class)会返回User对应的TableInfo,里面包含:

  • 表名(如tb_sys_user
  • 主键字段
  • 所有加了@TableField(fill = ...)的字段
  • 逻辑删除字段
  • 其他元数据

getTableInfo(LambdaUpdateWrapper.class)会返回null,因为LambdaUpdateWrapper不是实体类,没有 @TableName 注解,MyBatis-Plus不知道它对应哪张表,也就意味着tableInfonull,即哪张表都不知道,自然也无法判断哪些字段需要自动填充。此时直接返回原始参数,跳过updateFill,当不为null时,就会执行:

  1. tableInfo里找出所有fill = FieldFill.INSERT / INSERT_UPDATE / UPDATE的字段;
  2. 把这些字段信息封装成MetaObject
  3. 调用自定义的MybatisPlusMetaObjectHandler.updateFill(metaObject)
  4. 在方法里通过 strictInsertFill / strictUpdateFill 设置的值,最终会被写回实体对象

MyBatis-Plus插件

分页插件

如果MyBatisPlus的版本是3.5.9以上的,那么需要引入下面的jsqlparser依赖

XML
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-jsqlparser -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-jsqlparser</artifactId>
    <version>3.5.14</version>
</dependency>

先创建配置类:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Configuration
public class MybatisPlusConfig {

    // 分页插件
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInnerInterceptor.setMaxLimit(1000L);
        mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
        return mybatisPlusInterceptor;
    }
}

MyBatis-Plus要实现分页查询需要指定两个参数:

  1. 当前页码
  2. 每一页的条目个数

在查询前需要先创建一个Page类对象,将上面两个参数传入构造函数中。接着再调用selectPage方法传入创建好的Page类对象

但是selectPage默认是需要传递两个参数的,如果是无条件查询,那么第二个参数传递null即可

创建测试类如下:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@SpringBootTest
public class MybatisPlusPageApplicationTests {

    @Autowired
    private BookInfoMapper bookInfoMapper;

    @Test
    public void test() {
        Page<BookInfo> page = new Page<>(3, 10);
        // 无条件查询
        Page<BookInfo> result = bookInfoMapper.selectPage(page, null);
        // 打印分页信息(调试用)
        System.out.println("当前页: " + result.getCurrent());
        System.out.println("每页大小: " + result.getSize());
        System.out.println("总记录数: " + result.getTotal());
        System.out.println("总页数: " + result.getPages());
        System.out.println("数据列表: " + result.getRecords());
    }
}

可以看到输出结果如下:

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
当前页: 1
每页大小: 10
总记录数: 27
总页数: 3
数据列表: [
BookInfo(id=1, bookName=活着, author=余华, count=29, price=22.00, publish=北京文艺出版社, status=1, createTime=Sun Sep 14 19:38:41 CST 2025, updateTime=Mon Sep 15 19:50:52 CST 2025), 
BookInfo(id=2, bookName=平凡的世界, author=路遥, count=5, price=98.56, publish=北京十月文艺出版社, status=1, createTime=Sun Sep 14 19:38:41 CST 2025, updateTime=Mon Sep 15 19:52:02 CST 2025), 
BookInfo(id=3, bookName=三体, author=刘慈欣, count=9, price=102.67, publish=重庆出版社, status=1, createTime=Sun Sep 14 19:38:41 CST 2025, updateTime=Mon Sep 15 19:52:09 CST 2025), 
BookInfo(id=4, bookName=金字塔原理, author=麦肯锡, count=16, price=178.00, publish=民主与建设出版社, status=1, createTime=Sun Sep 14 19:38:41 CST 2025, updateTime=Mon Sep 15 19:54:32 CST 2025), 
BookInfo(id=5, bookName=云边有个小卖部, author=张嘉佳, count=1, price=54.00, publish=湖南文艺出版社, status=1, createTime=Sun Sep 14 21:35:55 CST 2025, updateTime=Sun Sep 14 21:41:07 CST 2025), 
BookInfo(id=6, bookName=被讨厌的勇气:“自我启发之父”阿德勒的哲学课, author=岸见一郎, count=1, price=58.00, publish=机械工业出版社, status=1, createTime=Mon Sep 15 18:15:04 CST 2025, updateTime=Mon Sep 15 18:15:04 CST 2025), 
BookInfo(id=7, bookName=流浪地球, author=刘慈欣, count=1, price=87.00, publish=四川科学技术出版社, status=1, createTime=Mon Sep 15 18:16:33 CST 2025, updateTime=Mon Sep 15 19:55:21 CST 2025), 
BookInfo(id=8, bookName=图书2, author=作者2, count=29, price=22.00, publish=出版社2, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Sep 15 21:26:39 CST 2025), 
BookInfo(id=9, bookName=图书3, author=作者2, count=29, price=22.00, publish=出版社 3, status=0, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Sat Sep 20 17:38:50 CST 2025), 
BookInfo(id=10, bookName=图书4, author=作者2, count=29, price=22.00, publish=出版社1, status=0, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Sat Sep 20 14:50:07 CST 2025)
]

如果传递的当前页参数为2,那么就是从第11条开始到第20条数据,可以得到下面的结果:

Text Only
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
当前页: 2
每页大小: 10
总记录数: 27
总页数: 3
数据列表: [
BookInfo(id=11, bookName=图书5, author=作者2, count=29, price=22.00, publish=出版社1, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Oct 20 12:29:11 CST 2025), 
BookInfo(id=12, bookName=图书6, author=作者2, count=29, price=22.00, publish=出版社1, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Sep 15 21:26:39 CST 2025), 
BookInfo(id=13, bookName=图书7, author=作者2, count=29, price=22.00, publish=出版社1, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Oct 20 12:29:13 CST 2025), 
BookInfo(id=14, bookName=图书8, author=作者2, count=29, price=22.00, publish=出版社1, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Sep 15 21:26:39 CST 2025), 
BookInfo(id=15, bookName=图书9, author=作者2, count=29, price=22.00, publish=出版社1, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Oct 20 12:29:16 CST 2025), 
BookInfo(id=16, bookName=图书10, author=作者2, count=29, price=22.00, publish=出版社1, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Sep 15 21:26:39 CST 2025), 
BookInfo(id=17, bookName=图书11, author=作者2, count=29, price=22.00, publish=出版社1, status=0, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Oct 20 12:29:20 CST 2025), 
BookInfo(id=18, bookName=图书12, author=作者2, count=29, price=22.00, publish=出版社1, status=0, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Sat Sep 20 14:52:12 CST 2025), 
BookInfo(id=19, bookName=图书13, author=作者2, count=29, price=22.00, publish=出版社1, status=0, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Oct 20 12:29:24 CST 2025), 
BookInfo(id=20, bookName=图书14, author=作者2, count=29, price=22.00, publish=出版社1, status=1, createTime=Mon Sep 15 21:26:39 CST 2025, updateTime=Mon Sep 15 21:26:39 CST 2025)
]

根据官方文档的描述,Page类介绍如下:

Page 类继承了 IPage 类,实现了简单分页模型。如果你需要实现自己的分页模型,可以继承 Page 类或实现 IPage 类。Page类中有以下的属性:

属性名 类型 默认值 描述
records List<T> emptyList 查询数据列表
total Long 0 查询列表总记录数
size Long 10 每页显示条数,默认 10
current Long 1 当前页
orders List<OrderItem> emptyList 排序字段信息
optimizeCountSql boolean true 自动优化 COUNT SQL
optimizeJoinOfCountSql boolean true 自动优化 COUNT SQL 是否把 join 查询部分移除
searchCount boolean true 是否进行 count 查询
maxLimit Long 单页分页条数限制
countId String XML 自定义 count 查询的 statementId

如果想根据具体条件查询,则可以使用MyBatis-Plus的条件构造器,例如根据出版社名称不为“出版社1”的结果进行分页查询,每页三条,则可以写为:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Test
public void test1() {
    Page<BookInfo> page = new Page<>(1, 3);
    // 构建出版社名称不为“出版社1”的条件构造器
    LambdaQueryWrapper<BookInfo> publisher = new LambdaQueryWrapper<BookInfo>()
                                            .ne(BookInfo::getPublish, "出版社1");
    // 无条件查询
    Page<BookInfo> result = bookInfoMapper.selectPage(page, publisher);
    // 打印分页信息(调试用)
    System.out.println("当前页: " + result.getCurrent());
    System.out.println("每页大小: " + result.getSize());
    System.out.println("总记录数: " + result.getTotal());
    System.out.println("总页数: " + result.getPages());
    System.out.println("数据列表: " + result.getRecords());
}

输出结果如下:

Text Only
1
2
3
4
5
6
7
8
9
当前页: 1
每页大小: 3
总记录数: 9
总页数: 3
数据列表: [
BookInfo(id=1, bookName=活着, author=余华, count=29, price=22.00, publish=北京文艺出版社, status=1, createTime=Sun Sep 14 19:38:41 CST 2025, updateTime=Mon Sep 15 19:50:52 CST 2025), 
BookInfo(id=2, bookName=平凡的世界, author=路遥, count=5, price=98.56, publish=北京十月文艺出版社, status=1, createTime=Sun Sep 14 19:38:41 CST 2025, updateTime=Mon Sep 15 19:52:02 CST 2025), 
BookInfo(id=3, bookName=三体, author=刘慈欣, count=9, price=102.67, publish=重庆出版社, status=1, createTime=Sun Sep 14 19:38:41 CST 2025, updateTime=Mon Sep 15 19:52:09 CST 2025)
]

防止全表更新/删除插件

BlockAttackInnerInterceptor是MyBatis-Plus提供的一个安全防御型插件,用于防止因误操作或恶意攻击导致的全表更新/删除。当执行的UPDATEDELETE语句没有WHERE条件时,该插件会抛出异常,阻止 SQL 执行

BlockAttackInnerInterceptor 基于 JSqlParser 解析 SQL,因此需要保证项目中已经引入 jsqlparser 依赖。MyBatis-Plus 3.5.9 及以上版本建议显式引入 mybatis-plus-jsqlparser

在已有的MybatisPlusConfig配置类中添加BlockAttackInnerInterceptor

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Configuration
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        // 分页插件
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        paginationInnerInterceptor.setMaxLimit(1000L);
        mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
        // 防止全表更新/删除插件
        mybatisPlusInterceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

创建测试类,分别执行不带条件的updatedelete操作:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@SpringBootTest
public class MybatisPlusBlockAttackApplicationTests {

    @Autowired
    private BookInfoMapper bookInfoMapper;

    @Test
    public void testUpdate() {
        BookInfo bookInfo = new BookInfo();
        bookInfo.setPrice(new BigDecimal("9.90"));
        // 未指定 WHERE 条件,会被拦截
        bookInfoMapper.update(bookInfo, null);
    }

    @Test
    public void testDelete() {
        // 未指定 WHERE 条件,会被拦截
        bookInfoMapper.delete(null);
    }
}

执行上述测试方法后,控制台会抛出如下异常:

Text Only
1
com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table deletion

如果是全表更新,抛出的异常信息为:

Text Only
1
com.baomidou.mybatisplus.core.exceptions.MybatisPlusException: Prohibition of table update

如果某个 Mapper 方法确实需要执行全表操作,可以在对应方法上添加@InterceptorIgnore(blockAttack = "true")注解临时绕过拦截