简介
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 | 字符串字段名形式的查询条件构造器 |
| LambdaQueryWrapper | Lambda 方法引用形式的查询条件构造器 |
| UpdateWrapper | 字符串字段名形式的更新条件构造器 |
| LambdaUpdateWrapper | Lambda 方法引用形式的更新条件构造器 |
| 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:开发环境打印 SQLid-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_UUID | UUID 字符串 | 字符串主键 |
| IdType.INPUT | 手动传入主键 | 外部系统同步数据 |
如果数据库字段是 AUTO_INCREMENT,实体里通常写:
1@TableId(type = IdType.AUTO) 2private Long id; 3
自动填充
createTime、updateTime 这类字段经常需要自动填充。
实体字段上先配置:
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
除了 BaseMapper,MyBatis-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 的区别
| 对比项 | JdbcTemplate | MyBatis | MyBatis-Plus | MyBatis-Flex |
|---|---|---|---|---|
| SQL 控制 | 很直接 | 很直接 | 支持 XML 和 Wrapper | 支持 XML 和 QueryWrapper |
| 单表 CRUD | 手写 | 手写 | 内置 | 内置 |
| Service 封装 | 无 | 无 | IService | IService |
| 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 和分页写好数据层》 是转载文章,点击查看原文。