Java MyBatis-Plus 实战指南:用 BaseMapper、Wrapper 和分页写好数据层

作者:唐青枫日期:2026/6/8

简介

MyBatis-Plus 是一个基于 MyBatis 的增强工具。

它经常被简称为 MP

它的核心定位是:

1只做增强,不改变 MyBatis 原有能力。
2

普通 MyBatis 项目里,哪怕只是做一张表的增删改查,也经常要写:

1Mapper 接口
2Mapper XML
3insert SQL
4delete SQL
5update SQL
6select SQL
7分页 SQL
8条件 SQL
9

MyBatis-Plus 把这些单表常规操作封装成了通用方法。

最常见的写法是:

1public interface UserMapper extends BaseMapper<User> {
2}
3

继承 BaseMapper<User> 后,就可以直接使用:

1userMapper.insert(user);
2userMapper.selectById(1L);
3userMapper.updateById(user);
4userMapper.deleteById(1L);
5

一句话概括:

1MyBatis-Plus 用来减少 MyBatis 项目里的重复 CRUD 代码,同时保留 XML、自定义 SQL、Mapper 扩展这些原生能力。
2

MyBatis-Plus 适合什么场景

它适合这些数据访问层场景:

  • 单表 CRUD 很多
  • 后台管理系统列表页很多
  • 查询条件经常动态组合
  • 需要分页、逻辑删除、自动填充、乐观锁
  • 项目已经在使用 MyBatis
  • 复杂 SQL 仍然希望写 XML

大致可以这样理解:

1简单单表操作交给 BaseMapper
2动态条件交给 Wrapper
3分页交给分页插件
4通用 Service 交给 IService
5复杂 SQL 继续写 MyBatis XML
6

核心概念

名称作用
@TableName指定实体类对应的表名
@TableId指定主键字段和主键策略
@TableField指定字段映射、自动填充、忽略字段等
@TableLogic指定逻辑删除字段
@Version指定乐观锁版本字段
BaseMapper<T>Mapper 层通用 CRUD
IService<T>Service 层通用方法接口
ServiceImpl<M, T>Service 层通用实现
QueryWrapper字符串字段名形式的查询条件构造器
LambdaQueryWrapperLambda 方法引用形式的查询条件构造器
UpdateWrapper字符串字段名形式的更新条件构造器
LambdaUpdateWrapperLambda 方法引用形式的更新条件构造器
Page<T>分页参数和分页结果

最常用的组合是:

1@TableName + @TableId + BaseMapper + LambdaQueryWrapper + Page
2

Maven 依赖

MyBatis-Plus 需要根据 Spring Boot 版本选择 starter。

Spring Boot 2.x

1<dependency>
2    <groupId>com.baomidou</groupId>
3    <artifactId>mybatis-plus-boot-starter</artifactId>
4    <version>3.5.15</version>
5</dependency>
6

Spring Boot 3.x

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

Spring Boot 4.x

1<dependency>
2    <groupId>com.baomidou</groupId>
3    <artifactId>mybatis-plus-spring-boot4-starter</artifactId>
4    <version>3.5.15</version>
5</dependency>
6

数据库驱动以 MySQL 为例:

1<dependency>
2    <groupId>com.mysql</groupId>
3    <artifactId>mysql-connector-j</artifactId>
4    <scope>runtime</scope>
5</dependency>
6

3.5.9 开始,分页插件相关依赖被拆成可选模块。

如果需要使用分页插件,还需要引入:

1<dependency>
2    <groupId>com.baomidou</groupId>
3    <artifactId>mybatis-plus-jsqlparser</artifactId>
4    <version>3.5.15</version>
5</dependency>
6

如果项目仍然运行在 JDK 8,可以按官方说明选择 mybatis-plus-jsqlparser-4.9

数据源配置

application.yml 示例:

1spring:
2  datasource:
3    url: jdbc:mysql://localhost:3306/mp_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
4    username: root
5    password: 123456
6    driver-class-name: com.mysql.cj.jdbc.Driver
7
8mybatis-plus:
9  configuration:
10    map-underscore-to-camel-case: true
11    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
12  global-config:
13    db-config:
14      id-type: auto
15      logic-delete-field: deleted
16      logic-delete-value: 1
17      logic-not-delete-value: 0
18

几个常见配置:

  • map-underscore-to-camel-case:下划线字段映射驼峰属性
  • log-impl:开发环境打印 SQL
  • id-type:全局主键策略
  • logic-delete-field:全局逻辑删除字段

开发环境打开 SQL 日志很方便。

生产环境通常交给日志系统统一控制。

启动类配置

启动类加上 @MapperScan

1import org.mybatis.spring.annotation.MapperScan;
2import org.springframework.boot.SpringApplication;
3import org.springframework.boot.autoconfigure.SpringBootApplication;
4
5@SpringBootApplication
6@MapperScan("com.example.demo.mapper")
7public class MyBatisPlusDemoApplication {
8
9    public static void main(String[] args) {
10        SpringApplication.run(MyBatisPlusDemoApplication.class, args);
11    }
12}
13

也可以在每个 Mapper 接口上加 @Mapper

Mapper 较多时,@MapperScan 更省事。

分页插件配置

使用分页功能时,需要配置 MybatisPlusInterceptor

1import com.baomidou.mybatisplus.annotation.DbType;
2import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
3import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
4import org.springframework.context.annotation.Bean;
5import org.springframework.context.annotation.Configuration;
6
7@Configuration
8public class MyBatisPlusConfig {
9
10    @Bean
11    public MybatisPlusInterceptor mybatisPlusInterceptor() {
12        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
13        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
14        return interceptor;
15    }
16}
17

如果同时配置多个插件,分页插件通常放在最后。

准备演示表

下面用用户表做示例。

1DROP TABLE IF EXISTS sys_user;
2
3CREATE TABLE sys_user (
4  id BIGINT PRIMARY KEY AUTO_INCREMENT,
5  username VARCHAR(50) NOT NULL,
6  email VARCHAR(100) NOT NULL,
7  age INT NOT NULL,
8  status VARCHAR(20) NOT NULL,
9  deleted TINYINT NOT NULL DEFAULT 0,
10  version INT NOT NULL DEFAULT 0,
11  create_time DATETIME NOT NULL,
12  update_time DATETIME NULL
13);
14
15INSERT INTO sys_user (username, email, age, status, deleted, version, create_time, update_time) VALUES
16('张三', 'zhangsan@example.com', 20, 'ACTIVE', 0, 0, '2026-01-01 10:00:00', NULL),
17('李四', 'lisi@example.com', 25, 'ACTIVE', 0, 0, '2026-01-02 10:00:00', NULL),
18('王五', 'wangwu@example.com', 17, 'DISABLED', 0, 0, '2026-01-03 10:00:00', NULL);
19

实体类

1package com.example.demo.entity;
2
3import com.baomidou.mybatisplus.annotation.FieldFill;
4import com.baomidou.mybatisplus.annotation.IdType;
5import com.baomidou.mybatisplus.annotation.TableField;
6import com.baomidou.mybatisplus.annotation.TableId;
7import com.baomidou.mybatisplus.annotation.TableLogic;
8import com.baomidou.mybatisplus.annotation.TableName;
9import com.baomidou.mybatisplus.annotation.Version;
10
11import java.time.LocalDateTime;
12
13@TableName("sys_user")
14public class User {
15
16    @TableId(type = IdType.AUTO)
17    private Long id;
18
19    private String username;
20
21    private String email;
22
23    private Integer age;
24
25    private String status;
26
27    @TableLogic
28    private Integer deleted;
29
30    @Version
31    private Integer version;
32
33    @TableField(fill = FieldFill.INSERT)
34    private LocalDateTime createTime;
35
36    @TableField(fill = FieldFill.INSERT_UPDATE)
37    private LocalDateTime updateTime;
38
39    public Long getId() {
40        return id;
41    }
42
43    public void setId(Long id) {
44        this.id = id;
45    }
46
47    public String getUsername() {
48        return username;
49    }
50
51    public void setUsername(String username) {
52        this.username = username;
53    }
54
55    public String getEmail() {
56        return email;
57    }
58
59    public void setEmail(String email) {
60        this.email = email;
61    }
62
63    public Integer getAge() {
64        return age;
65    }
66
67    public void setAge(Integer age) {
68        this.age = age;
69    }
70
71    public String getStatus() {
72        return status;
73    }
74
75    public void setStatus(String status) {
76        this.status = status;
77    }
78
79    public Integer getDeleted() {
80        return deleted;
81    }
82
83    public void setDeleted(Integer deleted) {
84        this.deleted = deleted;
85    }
86
87    public Integer getVersion() {
88        return version;
89    }
90
91    public void setVersion(Integer version) {
92        this.version = version;
93    }
94
95    public LocalDateTime getCreateTime() {
96        return createTime;
97    }
98
99    public void setCreateTime(LocalDateTime createTime) {
100        this.createTime = createTime;
101    }
102
103    public LocalDateTime getUpdateTime() {
104        return updateTime;
105    }
106
107    public void setUpdateTime(LocalDateTime updateTime) {
108        this.updateTime = updateTime;
109    }
110}
111

几个重点:

  • @TableName("sys_user"):指定表名
  • @TableId(type = IdType.AUTO):主键使用数据库自增
  • @TableLogic:逻辑删除字段
  • @Version:乐观锁字段
  • @TableField(fill = FieldFill.INSERT):插入时自动填充
  • @TableField(fill = FieldFill.INSERT_UPDATE):插入和更新时自动填充

主键策略

常见主键策略有这些:

策略说明常见场景
IdType.AUTO数据库自增单库单表、传统业务表
IdType.ASSIGN_ID雪花算法生成 ID分布式系统、业务主键
IdType.ASSIGN_UUIDUUID 字符串字符串主键
IdType.INPUT手动传入主键外部系统同步数据

如果数据库字段是 AUTO_INCREMENT,实体里通常写:

1@TableId(type = IdType.AUTO)
2private Long id;
3

自动填充

createTimeupdateTime 这类字段经常需要自动填充。

实体字段上先配置:

1@TableField(fill = FieldFill.INSERT)
2private LocalDateTime createTime;
3
4@TableField(fill = FieldFill.INSERT_UPDATE)
5private LocalDateTime updateTime;
6

再实现 MetaObjectHandler

1package com.example.demo.config;
2
3import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
4import org.apache.ibatis.reflection.MetaObject;
5import org.springframework.stereotype.Component;
6
7import java.time.LocalDateTime;
8
9@Component
10public class MyMetaObjectHandler implements MetaObjectHandler {
11
12    @Override
13    public void insertFill(MetaObject metaObject) {
14        strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
15        strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
16    }
17
18    @Override
19    public void updateFill(MetaObject metaObject) {
20        strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
21    }
22}
23

插入时自动填充:

1create_time
2update_time
3

更新时自动填充:

1update_time
2

Mapper 接口

1package com.example.demo.mapper;
2
3import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4import com.example.demo.entity.User;
5
6public interface UserMapper extends BaseMapper<User> {
7}
8

继承 BaseMapper<User> 后,单表常用方法都可以直接使用。

新增数据

1User user = new User();
2user.setUsername("赵六");
3user.setEmail("zhaoliu@example.com");
4user.setAge(28);
5user.setStatus("ACTIVE");
6
7userMapper.insert(user);
8
9System.out.println(user.getId());
10

如果主键是自增,插入后会回填 id

常见 SQL 大致是:

1insert into sys_user (username, email, age, status, create_time, update_time)
2values (?, ?, ?, ?, ?, ?)
3

根据 ID 查询

1User user = userMapper.selectById(1L);
2

如果配置了逻辑删除,查询会自动带上未删除条件。

查询全部

1List<User> users = userMapper.selectList(null);
2

null 表示没有额外查询条件。

业务表数据量较大时,不适合直接查全部。

更常见的是条件查询或分页查询。

批量查询

1List<Long> ids = Arrays.asList(1L, 2L, 3L);
2
3List<User> users = userMapper.selectBatchIds(ids);
4

对应 SQL 大致是:

1select id, username, email, age, status, deleted, version, create_time, update_time
2from sys_user
3where id in (?, ?, ?)
4

按 Map 查询

selectByMap 可以按字段和值做等值查询。

1Map<String, Object> params = new HashMap<>();
2params.put("status", "ACTIVE");
3
4List<User> users = userMapper.selectByMap(params);
5

注意这里的 key 是数据库字段名,不是 Java 属性名。

比如字段是:

1create_time
2

就写:

1params.put("create_time", value);
2

根据 ID 修改

1User user = new User();
2user.setId(1L);
3user.setEmail("new-zhangsan@example.com");
4
5userMapper.updateById(user);
6

updateById 会根据主键更新。

未设置的字段通常不会参与更新。

根据 ID 删除

1userMapper.deleteById(1L);
2

如果没有配置逻辑删除,就是物理删除。

如果配置了 @TableLogic,会变成逻辑删除。

大致 SQL:

1update sys_user
2set deleted = 1
3where id = ?
4  and deleted = 0
5

逻辑删除后的数据,普通查询会自动过滤。

BaseMapper 常用方法

方法作用
insert(entity)新增一条数据
deleteById(id)按 ID 删除
delete(wrapper)按条件删除
updateById(entity)按 ID 更新
update(entity, wrapper)按条件更新
selectById(id)按 ID 查询
selectBatchIds(ids)按 ID 集合查询
selectByMap(map)按 Map 等值查询
selectOne(wrapper)查询一条
selectList(wrapper)查询列表
selectCount(wrapper)查询数量
selectPage(page, wrapper)分页查询

Wrapper 是什么

Wrapper 用来构造 SQL 条件。

普通 SQL:

1select *
2from sys_user
3where status = 'ACTIVE'
4  and age >= 18
5order by id desc
6

LambdaQueryWrapper 写法:

1LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
2wrapper.eq(User::getStatus, "ACTIVE")
3       .ge(User::getAge, 18)
4       .orderByDesc(User::getId);
5
6List<User> users = userMapper.selectList(wrapper);
7

LambdaQueryWrapper 使用方法引用:

1User::getStatus
2User::getAge
3User::getId
4

这样字段改名时,编译期更容易发现问题。

QueryWrapper 和 LambdaQueryWrapper

QueryWrapper 使用字符串字段名:

1QueryWrapper<User> wrapper = new QueryWrapper<>();
2wrapper.eq("status", "ACTIVE")
3       .ge("age", 18);
4

LambdaQueryWrapper 使用方法引用:

1LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
2wrapper.eq(User::getStatus, "ACTIVE")
3       .ge(User::getAge, 18);
4

日常业务代码里,LambdaQueryWrapper 更常用。

原因是字段名不会写成字符串。

常见条件写法

等于:

1wrapper.eq(User::getStatus, "ACTIVE");
2

不等于:

1wrapper.ne(User::getStatus, "DISABLED");
2

大于:

1wrapper.gt(User::getAge, 18);
2

大于等于:

1wrapper.ge(User::getAge, 18);
2

小于:

1wrapper.lt(User::getAge, 60);
2

模糊查询:

1wrapper.like(User::getUsername, "张");
2

范围查询:

1wrapper.between(User::getAge, 18, 30);
2

IN 查询:

1wrapper.in(User::getId, Arrays.asList(1L, 2L, 3L));
2

排序:

1wrapper.orderByDesc(User::getId);
2

只查询部分字段:

1wrapper.select(User::getId, User::getUsername, User::getEmail);
2

条件参数

很多 Wrapper 方法都有一个 condition 参数。

1wrapper.eq(status != null, User::getStatus, status);
2wrapper.like(keyword != null && !keyword.isBlank(), User::getUsername, keyword);
3wrapper.ge(minAge != null, User::getAge, minAge);
4

含义是:

1condition  true,才拼接这个条件。
2condition  false,跳过这个条件。
3

动态查询时很方便。

完整示例:

1public List<User> search(String keyword, String status, Integer minAge) {
2    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
3
4    wrapper.like(keyword != null && !keyword.isBlank(), User::getUsername, keyword)
5           .eq(status != null && !status.isBlank(), User::getStatus, status)
6           .ge(minAge != null, User::getAge, minAge)
7           .orderByDesc(User::getId);
8
9    return userMapper.selectList(wrapper);
10}
11

查询单个对象

按唯一字段查询:

1public User findByEmail(String email) {
2    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
3    wrapper.eq(User::getEmail, email);
4
5    return userMapper.selectOne(wrapper);
6}
7

如果可能查不到,可以返回 Optional

1public Optional<User> findOptionalByEmail(String email) {
2    LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
3    wrapper.eq(User::getEmail, email);
4
5    return Optional.ofNullable(userMapper.selectOne(wrapper));
6}
7

selectOne 适合结果最多一条的场景。

如果实际查出多条,会出现结果数量异常。

查询数量

1LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
2wrapper.eq(User::getStatus, "ACTIVE");
3
4Long count = userMapper.selectCount(wrapper);
5

返回值表示满足条件的数据条数。

分页查询

分页查询使用 Page<T>

1Page<User> page = new Page<>(1, 10);
2
3LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
4wrapper.eq(User::getStatus, "ACTIVE")
5       .orderByDesc(User::getId);
6
7Page<User> result = userMapper.selectPage(page, wrapper);
8

常用字段:

1List<User> records = result.getRecords();
2long total = result.getTotal();
3long current = result.getCurrent();
4long size = result.getSize();
5long pages = result.getPages();
6

含义:

1records:当前页数据
2total:总条数
3current:当前页
4size:每页条数
5pages:总页数
6

分页插件配置和 mybatis-plus-jsqlparser 依赖缺一不可。

不查总数的分页

有些列表只需要下一页,不需要总条数。

可以关闭 count 查询:

1Page<User> page = new Page<>(1, 10, false);
2
3Page<User> result = userMapper.selectPage(page, wrapper);
4

第三个参数是:

1searchCount
2

设置为 false 后,不再查询总数。

条件更新

LambdaUpdateWrapper 可以按条件更新。

1LambdaUpdateWrapper<User> wrapper = Wrappers.lambdaUpdate();
2wrapper.set(User::getStatus, "DISABLED")
3       .eq(User::getStatus, "ACTIVE")
4       .lt(User::getAge, 18);
5
6userMapper.update(null, wrapper);
7

大致 SQL:

1update sys_user
2set status = ?
3where status = ?
4  and age < ?
5  and deleted = 0
6

这种写法适合批量改状态、批量打标记。

条件删除

删除禁用状态用户:

1LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
2wrapper.eq(User::getStatus, "DISABLED");
3
4userMapper.delete(wrapper);
5

如果配置了逻辑删除,会执行逻辑删除。

IService 和 ServiceImpl

除了 BaseMapperMyBatis-Plus 还提供了通用 Service。

Service 接口:

1package com.example.demo.service;
2
3import com.baomidou.mybatisplus.extension.service.IService;
4import com.example.demo.entity.User;
5
6public interface UserService extends IService<User> {
7}
8

Service 实现:

1package com.example.demo.service.impl;
2
3import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
4import com.example.demo.entity.User;
5import com.example.demo.mapper.UserMapper;
6import com.example.demo.service.UserService;
7import org.springframework.stereotype.Service;
8
9@Service
10public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
11}
12

这样就能在 Service 层直接使用:

1userService.save(user);
2userService.getById(1L);
3userService.updateById(user);
4userService.removeById(1L);
5userService.list();
6userService.page(new Page<>(1, 10));
7

ServiceImpl 里面已经持有 Mapper。

简单业务可以直接复用通用方法。

复杂业务继续在 Service 里写自定义方法。

完整实战 Demo:UserService

1package com.example.demo.service;
2
3import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
5import com.baomidou.mybatisplus.core.toolkit.Wrappers;
6import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
7import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
8import com.example.demo.entity.User;
9import com.example.demo.mapper.UserMapper;
10import org.springframework.stereotype.Service;
11import org.springframework.transaction.annotation.Transactional;
12
13import java.util.Optional;
14
15@Service
16public class UserService extends ServiceImpl<UserMapper, User> {
17
18    @Transactional
19    public Long create(User user) {
20        user.setStatus("ACTIVE");
21
22        save(user);
23
24        return user.getId();
25    }
26
27    public Optional<User> findById(Long id) {
28        return Optional.ofNullable(getById(id));
29    }
30
31    public Page<User> pageUsers(String keyword, String status, Integer minAge, long current, long size) {
32        LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
33
34        wrapper.like(keyword != null && !keyword.isBlank(), User::getUsername, keyword)
35               .eq(status != null && !status.isBlank(), User::getStatus, status)
36               .ge(minAge != null, User::getAge, minAge)
37               .orderByDesc(User::getId);
38
39        return page(new Page<>(current, size), wrapper);
40    }
41
42    @Transactional
43    public void updateEmail(Long id, String email) {
44        User user = new User();
45        user.setId(id);
46        user.setEmail(email);
47
48        updateById(user);
49    }
50
51    @Transactional
52    public void disable(Long id) {
53        LambdaUpdateWrapper<User> wrapper = Wrappers.lambdaUpdate();
54        wrapper.set(User::getStatus, "DISABLED")
55               .eq(User::getId, id);
56
57        update(wrapper);
58    }
59
60    @Transactional
61    public void remove(Long id) {
62        removeById(id);
63    }
64}
65

这个 Service 包含:

  • 新增用户
  • 按 ID 查询
  • 动态条件分页
  • 修改邮箱
  • 禁用用户
  • 删除用户

Controller 示例

1package com.example.demo.controller;
2
3import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
4import com.example.demo.entity.User;
5import com.example.demo.service.UserService;
6import org.springframework.web.bind.annotation.DeleteMapping;
7import org.springframework.web.bind.annotation.GetMapping;
8import org.springframework.web.bind.annotation.PathVariable;
9import org.springframework.web.bind.annotation.PostMapping;
10import org.springframework.web.bind.annotation.PutMapping;
11import org.springframework.web.bind.annotation.RequestBody;
12import org.springframework.web.bind.annotation.RequestMapping;
13import org.springframework.web.bind.annotation.RequestParam;
14import org.springframework.web.bind.annotation.RestController;
15
16@RestController
17@RequestMapping("/users")
18public class UserController {
19
20    private final UserService userService;
21
22    public UserController(UserService userService) {
23        this.userService = userService;
24    }
25
26    @PostMapping
27    public Long create(@RequestBody User user) {
28        return userService.create(user);
29    }
30
31    @GetMapping("/{id}")
32    public User detail(@PathVariable Long id) {
33        return userService.findById(id)
34                .orElseThrow(() -> new IllegalArgumentException("用户不存在"));
35    }
36
37    @GetMapping
38    public Page<User> page(@RequestParam(required = false) String keyword,
39                           @RequestParam(required = false) String status,
40                           @RequestParam(required = false) Integer minAge,
41                           @RequestParam(defaultValue = "1") long current,
42                           @RequestParam(defaultValue = "10") long size) {
43        return userService.pageUsers(keyword, status, minAge, current, size);
44    }
45
46    @PutMapping("/{id}/email")
47    public void updateEmail(@PathVariable Long id, @RequestParam String email) {
48        userService.updateEmail(id, email);
49    }
50
51    @PutMapping("/{id}/disable")
52    public void disable(@PathVariable Long id) {
53        userService.disable(id);
54    }
55
56    @DeleteMapping("/{id}")
57    public void remove(@PathVariable Long id) {
58        userService.remove(id);
59    }
60}
61

自定义 Mapper SQL

MyBatis-Plus 不影响原生 MyBatis。

Mapper 可以继续写自定义方法:

1package com.example.demo.mapper;
2
3import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5import com.example.demo.entity.User;
6import org.apache.ibatis.annotations.Param;
7
8public interface UserMapper extends BaseMapper<User> {
9
10    Page<User> selectActiveUserPage(Page<User> page, @Param("keyword") String keyword);
11}
12

XML:

1<select id="selectActiveUserPage" resultType="com.example.demo.entity.User">
2    select id, username, email, age, status, deleted, version, create_time, update_time
3    from sys_user
4    where deleted = 0
5      and status = 'ACTIVE'
6      and (
7        username like concat('%', #{keyword}, '%')
8        or email like concat('%', #{keyword}, '%')
9      )
10    order by id desc
11</select>
12

调用:

1Page<User> page = new Page<>(1, 10);
2Page<User> result = userMapper.selectActiveUserPage(page, "张");
3

分页插件会对自定义 SQL 生效。

逻辑删除

逻辑删除字段:

1@TableLogic
2private Integer deleted;
3

全局配置:

1mybatis-plus:
2  global-config:
3    db-config:
4      logic-delete-field: deleted
5      logic-delete-value: 1
6      logic-not-delete-value: 0
7

调用删除:

1userMapper.deleteById(1L);
2

实际变成:

1update sys_user
2set deleted = 1
3where id = ?
4  and deleted = 0
5

普通查询会自动过滤:

1deleted = 0
2

逻辑删除适合用户、订单、文章这类需要保留历史记录的数据。

乐观锁

乐观锁字段:

1@Version
2private Integer version;
3

需要配置乐观锁插件:

1import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
2import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
3import org.springframework.context.annotation.Bean;
4import org.springframework.context.annotation.Configuration;
5
6@Configuration
7public class MyBatisPlusConfig {
8
9    @Bean
10    public MybatisPlusInterceptor mybatisPlusInterceptor() {
11        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
12        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
13        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
14        return interceptor;
15    }
16}
17

更新时会带上版本条件。

大致逻辑:

1where id = ? and version = ?
2

更新成功后,版本号增加。

如果影响行数为 0,说明数据已经被其他事务改过。

防止全表更新和删除

可以配置 BlockAttackInnerInterceptor

1import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
2
3@Bean
4public MybatisPlusInterceptor mybatisPlusInterceptor() {
5    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
6    interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
7    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
8    return interceptor;
9}
10

它可以拦截没有条件的全表更新和删除。

比如:

1delete from sys_user
2

或:

1update sys_user set status = 'DISABLED'
2

这类操作在业务系统里通常风险很高。

插件顺序

多个插件同时使用时,顺序需要注意。

常见建议:

1多租户、动态表名
2分页、乐观锁
3SQL 规范、防全表更新删除
4

分页插件一般放在靠后位置。

原因是分页需要基于前面已经改写后的 SQL 再处理。

和 MyBatis、JdbcTemplate、MyBatis-Flex 的区别

对比项JdbcTemplateMyBatisMyBatis-PlusMyBatis-Flex
SQL 控制很直接很直接支持 XML 和 Wrapper支持 XML 和 QueryWrapper
单表 CRUD手写手写内置内置
Service 封装IServiceIService
Lambda 查询支持支持 APT 表定义
分页手写或插件插件内置插件内置分页
生态成熟度简单稳定成熟成熟较新
适合场景少量 SQL、工具类项目SQL 控制要求高常规后台 CRUD轻量增强、灵活查询

粗略理解:

1JdbcTemplate 更接近 JDBC
2MyBatis 更强调 SQL 映射
3MyBatis-Plus 更强调通用 CRUD 和成熟生态
4MyBatis-Flex 更强调轻量和灵活查询构造
5

常见使用建议

Mapper 层保持简单

Mapper 层适合放:

  • BaseMapper 基础能力
  • 少量自定义 SQL 方法
  • 和数据库强相关的查询

业务流程、事务、跨表组合,更适合放在 Service 层。

Lambda Wrapper 优先

相比字符串字段名:

1wrapper.eq("username", "张三");
2

Lambda 写法更容易维护:

1wrapper.eq(User::getUsername, "张三");
2

字段重命名后,编译器可以帮忙发现问题。

查询列表尽量带条件或分页

1userMapper.selectList(null);
2

这会查询所有未逻辑删除的数据。

对于业务表,数据量增长后很容易变慢。

更常见的做法:

1userMapper.selectList(wrapper);
2userMapper.selectPage(page, wrapper);
3

复杂 SQL 回到 XML

Wrapper 适合中等复杂度条件查询。

如果 SQL 包含大量聚合、窗口函数、复杂子查询、多层动态条件,XML 通常更清楚。

MyBatis-Plus 不限制原生 MyBatis 写法。

批量操作注意分批

saveBatch 很方便,但数据量很大时仍然要拆批。

常见做法:

1 500 条或 1000 条执行一次。
2

这样可以减少 SQL 太长、事务太大、锁持有时间过长等问题。

常用方法汇总

方法作用常见场景
insert(entity)新增数据创建用户
deleteById(id)按 ID 删除删除单条记录
delete(wrapper)按条件删除批量删除、逻辑删除
updateById(entity)按 ID 更新修改单条记录
update(entity, wrapper)按条件更新批量改状态
selectById(id)按 ID 查询详情页
selectBatchIds(ids)批量 ID 查询批量加载
selectList(wrapper)条件列表查询列表页
selectOne(wrapper)查询单条唯一字段查询
selectCount(wrapper)查询数量统计
selectPage(page, wrapper)分页查询后台列表
save(entity)Service 新增业务层新增
saveBatch(list)Service 批量新增批量导入
page(page, wrapper)Service 分页分页接口
removeById(id)Service 删除删除接口

总结

MyBatis-Plus 的重点不是替代 MyBatis,而是把常规 CRUD 和条件查询封装得更顺手。

落地时抓住这条线就够了:

1实体类用 @TableName、@TableId、@TableLogic
2Mapper 继承 BaseMapper
3查询条件用 LambdaQueryWrapper
4分页配置 MybatisPlusInterceptor
5Service 复用 IService  ServiceImpl
6复杂 SQL 继续写 XML
7

它适合后台管理系统、业务中台、内部系统、常规 CRUD 较多的项目。

只要控制好 Wrapper 和 XML 的边界,数据访问层会比纯 MyBatis 少很多重复代码。


Java MyBatis-Plus 实战指南:用 BaseMapper、Wrapper 和分页写好数据层》 是转载文章,点击查看原文


相关推荐


基因泰克:检测级虚拟细胞基准!大语言模型+智能体
Omics Pro2026/6/1

摘要 机器学习与大规模生物数据的进展重新激发了构建虚拟细胞(预测细胞行为的计算模型,可加速生物学发现)的研究前景。该愿景的核心应用是体外表型筛选,即模型预测细胞扰动在未知生物场景下的效应,该任务融合异质文本输入与多样表型输出,高度适配大语言模型与智能体系统。但目前该任务缺乏标准化基准,现有研究仅聚焦分子层面读数,与真实药物研发流程中的表型终点脱节。本研究推出基于1,920个公开CRISPR筛选构建的表型筛选预测基准AssayBench,覆盖5大类细胞表型;将筛选预测任务定义为单筛选基因排序任务


Spring MVC 的核心知识点梳理
huohuopro2026/5/11

MVC 是什么 MVC 不是 Spring 发明的,而是一种设计模式,目的是“解耦”。 M(Model,模型):数据 + 业务逻辑。比如 Teacher 类,TeacherService。V(View,视图):展示数据的界面。比如 JSP、Thymeleaf 模板,或者是现代返回 JSON 的前端页面。C(Controller,控制器):接收用户请求,调用 Model,最后选择 View 来展示。 流程:用户点击一个链接 → Controller 拿到请求 → 调 Service 拿到数据(Mo


精准医学的数据平台化与Python编程实战(中)
Allen_Lyb2026/5/1

第五章:高性能数据处理与分析 5.1 使用Pandas进行临床数据清洗与特征工程 import pandas as pd import numpy as np from sklearn.impute import SimpleImputer from sklearn.preprocessing import StandardScaler, OneHotEncoder # 加载模拟临床数据 df = pd.read_csv('clinical_cohort.csv') # 处理缺失值 nu


每天刷十几个平台的热榜太累了?我用一个页面全部搞定
扑克中的黑桃A2026/4/22

这里写目录标题 前言1.什么是DailyHot?2.在飞牛OS上部署DailyHot3.docker部署DailyHot4.使用DailyHot5.安装cpolar内网穿透6.配置公网地址7.保留固定公网地址总结 前言 每天看热点,你要打开几个 App? 微博、知乎、Hacker News、V2EX、豆瓣、少数派……每个平台都有热榜,每个平台都得点进去刷一遍。等把这些都刷完,半个小时过去了,真正有用的信息反而被稀释了。 而且还有个更现实的问题——你在公司想看看知乎热榜发生了啥,


LangGraph 入门到精通0x02:基础 API (二)
chaors2026/4/14

前言 今天继续学习一些 LangGraph 常用到的基础 API。废话少说上干货。 .点语法 Graph 的创建可以使用看着更简洁的 .语法。 graph = ( StateGraph(State) .add_node(a) .add_node(b) .add_edge(START, "a") .add_edge("a", "b") .add_edge("b", END) .compile() ) 步骤序列 StateGraph(St


你的 Android App 可能白白损失了 35% 的性能——R8 全模式配置详解
陆业聪2026/4/6

字节跳动的工程师优化启动速度时,可能花了数周分析 trace、改代码;Monzo 的团队却只改了一行配置,性能指标全线提升了 35%。这不是段子,是 Google 官方 blog 2026 年 3 月底发出来的案例。 问题来了:你的项目,是不是也开着 R8,但根本没用对? R8 到底做了什么——大多数人理解是错的 很多人对 R8 的理解停留在「代码混淆 + 压缩」。打开 minifyEnabled true,觉得任务完成了。 但 R8 实际上分两种工作模式: • 兼容模式(Compatibili


核心概念层——深入理解 Agent 是什么
想打游戏的程序猿2026/3/28

1 Agent vs ChatBot:从根本上理解区别 1.1 一个直观的例子 假设你对 AI 说:"帮我分析一下我们公司上周的销售数据,找出表现最好的产品,并给团队发一封总结邮件"。 ChatBot 的反应: ChatBot: "要分析销售数据,你可以按以下步骤操作: 1. 打开数据库,执行 SQL 查询获取上周的销售记录 2. 使用 Excel 或 Python 进行数据汇总 3. 找出销售额最高的产品 4. 撰写邮件总结发送给团队 你需要我帮你写 SQL 查询语句吗?" → ChatB


配置钉钉龙虾OpenClaw机器人调用OpenMetadata
光于前裕于后2026/3/20

目录 一、前言1️⃣钉钉(DingTalk)2️⃣OpenClaw3️⃣OpenMetadata4️⃣MCP(Model Context Protocol) 二、安装OpenClaw三、配置OpenClaw钉钉机器人四、调用OpenMetadata MCP 一、前言 先介绍下这四个工具/协议的定位与核心能力,本文将从零开始配置。 1️⃣钉钉(DingTalk) 阿里巴巴旗下的企业协作平台,2014年上线,是中国市场份额最大的企业即时通讯与办公套件之一。 核心能


10分钟搭建 Windows + WSL + Codex环境
Lei_official2026/3/12

并不是 AI 替代人,而是会用 AI 的人替代不会用 AI 的人。 我的大模型使用历程 从2023年秋季,我开始使用对话型的大模型,提升工作和学习的效率,以及回答一些生活上的常识问题。最开始是 ChatGPT 的免费版本,随着使用频率提高,慢慢会遇到问答超过上限的情况。随后便开通了Plus订阅直至今日。期间也曾使用过 Deepseek、Gemini、Minimax 等等,不过最主要的仍然是 ChatGPT,个人感觉它在回答的质量、速度、上下文方面体验最好。 在这段历程里,网页对话型 的 AI


MySQL中 SHOW FULL PROCESSLIST` 输出中 `State` 列的所有可能值
左Python右Java2026/3/4

SHOW FULL PROCESSLIST输出中State` 列的所有可能值,以及这些值代表的含义,这能帮你精准判断数据库连接的状态(包括锁相关、执行状态等)。 一、State 列核心分类及含义 State 列描述了当前线程正在执行的操作状态,不同状态对应不同的数据库行为,以下是最常见且实用的分类(按场景划分): 1. 锁相关状态(排查锁表核心) 这是你最关心的锁表相关状态,直接反映锁等待 / 阻塞: 状态值含义Waiting for t

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读