【保姆级教程】MyBatis-Plus 核心功能全解析(附实战代码)
Mybatis-plus
MyBatis-Plus(简称MP)是MyBatis的强力增强工具,在保留MyBatis所有特性的基础上,旨在简化开发、提升效率。它不是一个全新的框架,而是一个“站在巨人肩膀上”的国产开源组件。
1. 快速入门
1.1 配置环境
- 在SpringBoot环境中最常用,只需引入以下依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
对于3.5.5版本,推荐的SpringBoot版本为Spring Boot 2.7.x(最稳定)
支持的 Spring Boot 范围:Spring Boot 2.0.x ~ 3.0.x
2.普通 Spring 环境
可以使用基础依赖:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>3.5.5</version>
</dependency>
然后在 Spring XML 配置文件中手动配置。
- 非 Spring 环境
MyBatis-Plus 核心功能完全可以在纯 MyBatis 环境中使用,只需要:
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-core</artifactId>
<version>3.5.5</version>
</dependency>
1.2 简单实现(BaseMapper)和常见注解
MP通过扫描实体类,并基于反射获取实体类信息作为数据库表信息。普通的Mapper类必须继承BaseMapper类,并写出泛型,其中的User为实体类类名。
public interface UserMapper extends BaseMapper<User> {
}
1.在MP中有如下约定:
- 类名中大写转小写作为表名:MP 默认规则:实体类名直接作为表名(如实体类
UserInfo→ 表名userinfo),不会自动驼峰转 下划线,如类名与表名不匹配,可以使用**@TableName**来指定表名。 - 名为id的字段作为主键: 是 MP 默认行为 MP 会默认将实体类中名为 id 的字段识别为主键(无需加 @TableId);
若主键字段名不是 id(如 user_id),则必须加 @TableId 指定。 - 变量名驼峰转下划线作为表的字段名:这是 MyBatis 核心的默认规则(MP 完全继承)。
2.MS中比较常见的几个注解如下:
- @TableName:用来指定表名:实体类名与表名不一致时使用。
- @TableId:用来指定表中的主键字段信息。
- @TableField:用来指定表中的普通字段信息。
3.@TableId中idType枚举:
- AUTO:数据库自增长。
- INPUT:通过set方法自行输入。
- ASSIGN_ID(雪花算法):分配 ID,接口identifierGenerator的方法nextid来生成id,默认实现类为DefaultidentifierGenerator雪花算法。:补充一句 “雪花算法生成的是分布式唯一 ID,无需依赖数据库自增,适合分库分表场景;也就是说生成的ID如雪花一样每个都不一样”;
4.使用@TableField的常见场景:
- 成员变量名与数据库字段名不一致。
- 成员变量is 开头布尔字段匹配数据库无 is 字段:
private Boolean isVip;(匹配vip),只需在上方写上@TableField,无属性。 - 成员变量名与数据库关键字冲突。
- 成员变量不是数据库字段。
| 属性名 | 类型 | 默认值 | 作用说明 |
|---|---|---|---|
value |
String | “” | 指定数据库表中对应的字段名(解决字段名不一致) |
exist |
boolean | true | 标记该字段是否为数据库表中的真实字段(false = 非数据库字段) |
fill |
FieldFill | FieldFill.DEFAULT | 字段自动填充策略(如创建时间、更新时间自动赋值) |
select |
boolean | true | 是否参与查询(false = 查询时忽略该字段) |
update |
String | “” | 自定义更新语句的字段表达式(如update="%s+1"实现字段自增) |
insertStrategy |
FieldStrategy | DEFAULT | 插入时的字段验证策略(如忽略空值) |
updateStrategy |
FieldStrategy | DEFAULT | 更新时的字段验证策略 |
whereStrategy |
FieldStrategy | DEFAULT | 拼接 WHERE 条件时的字段验证策略 |
下面展示一段User实体类代码,展示一下MP的常见注解
// 主键注解:指定主键类型为自增(需数据库表id字段设置AUTO_INCREMENT)
// 如果不自主设置自增,而你往数据库添加新值的时候不传Id,那么就会走ASSIGN_ID,雪花算法
@TableId(type = IdType.AUTO)
private int id; // 主键ID
private String no; // 编号
private String name; // 姓名
private String password; // 密码
private int age; // 年龄
private int sex; // 性别(1-男,2-女等)
private String phone; // 手机号
// @TableField("role_id") // 明确告诉MyBatis-Plus,这个属性映射到数据库的“role_id”列
private int roleId; // 角色ID
private String isValid; // 是否有效(如Y/N)
//例子:
@TableField("is_name")
private boolen isName //如果不加此时会主动去除is,并去数据库中找name字段
@TableField("`order`")
private int order //此时order为数据库里面的关键字,会混淆,
//此时需要加上注释,里面填写`order`,并不是单独的order
@TableField(exist = false)
private int AAA //非数据库字段
对应的User表:
CREATE TABLE `user` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`no` VARCHAR(50) NOT NULL COMMENT '编号',
`name` VARCHAR(100) NOT NULL COMMENT '姓名',
`password` VARCHAR(255) NOT NULL COMMENT '密码',
`age` INT DEFAULT 0 COMMENT '年龄',
`sex` TINYINT DEFAULT 0 COMMENT '性别(1-男,2-女等)',
`phone` VARCHAR(20) DEFAULT '' COMMENT '手机号',
`role_id` INT NOT NULL COMMENT '角色ID',
`is_valid` VARCHAR(1) DEFAULT 'Y' COMMENT '是否有效(Y/N)',
`is_name` TINYINT(1) DEFAULT 0 COMMENT '是否名称',
`order` INT DEFAULT 0 COMMENT '排序',
PRIMARY KEY (`id`),
INDEX `idx_no` (`no`),
INDEX `idx_name` (`name`),
INDEX `idx_phone` (`phone`),
INDEX `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
1.3 常用配置
MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置。
下面是全局配置:
mybatis-plus:
type-aliases-package: com.liu.entity.User # 别名扫描包
mapper-locations: "classpath:/mapper/**/*.xml" # Mapper.xml文件地址,默认值,mapper包下面的所有xml文件都能识别到
configuration:
map-underscore-to-camel-case: true # 是否开启下划线和驼峰的映射
cache-enabled: false # 是否开启二级缓存
global-config:
db-config:
id-type: assign_id # id为雪花算法生成
update-strategy: not_null # 更新策略:只更新非空字段
这里只列举了一小部分,用到了去搜就可以。其实里面很多东西都不用自己去配置,很多都是默认的。如果你配置了注解,那么还是会首先按照你的注解来执行。
1.4 基础CRUD示例
我们需要编写一个测试类来执行MP里面基础的CRUD。
// 注入UserMapper后,快速测试
@Autowired
private UserMapper userMapper;
// 新增
User user = new User();
user.setName("测试");
userMapper.insert(user);
// 查询
User getById = userMapper.selectById(1L);
List<User> list = userMapper.selectList(null);
// 更新
user.setName("测试更新");
userMapper.updateById(user);
// 删除
userMapper.deleteById(1L);
值得注意的是:补充 “配置扫描 Mapper 接口”:SpringBoot 中需要加 @MapperScan("com.xxx.mapper")(主类上),新手容易漏这个步骤导致报错。
@MapperScan("com.liu.mapper")//在主启动类上面加上注解
@SpringBootApplication
public class SpringBootMpApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootMpApplication.class, args);
}
}
2. 核心功能
1. 条件构造器
MyBatis-Plus 的条件构造器(Wrapper) 是一套用于动态拼接 SQL 条件(WHERE/SET 子句) 的工具体系,核心目的是替代硬编码 SQL 条件,通过面向对象的链式调用生成灵活、可维护的查询 / 更新条件,避免手动拼接字符串带来的语法错误、SQL 注入风险。
下面是条件构造器的多个类:
Wrapper
- 最顶层的抽象基类,定义了所有条件构造器的通用方法(如
eq等于、like模糊查询等)。 - 不能直接实例化,仅作为父类提供基础能力。
AbstractWrapper
- 继承自
Wrapper的抽象子类,是条件构造的核心实现类,不能直接实例化使用。 - 封装了 SQL 的
WHERE子句逻辑(如条件拼接、逻辑运算符and/or等),是QueryWrapper、UpdateWrapper的直接父类。
QueryWrapper
- 继承自
AbstractWrapper,专门用于构建查询 / 删除的条件(对应 SQL 的SELECT/DELETE操作)。 - 支持链式调用添加条件(如
eq("name", "张三").gt("age", 18)),也可指定查询字段(select("id", "name"))。
UpdateWrapper
- 继承自
AbstractWrapper,专门用于构建更新的条件(对应 SQL 的UPDATE操作)。 - 除了条件,还支持直接设置更新字段(如
set("email", "new@xxx.com")),可同时写SET和WHERE子句。
AbstractLambdaWrapper
- 继承自
AbstractWrapper的抽象子类,提供Lambda 表达式支持的基础能力,不能直接实例化使用。 - 作用是让条件构造时可以通过 “实体类::属性” 的方式引用字段(避免硬写字段名字符串)。
LambdaQueryWrapper
- 继承自
AbstractLambdaWrapper,是QueryWrapper的Lambda 版本。 - 用 Lambda 表达式引用实体类属性(如
eq(User::getName, "张三")),编译时就能检查字段名是否正确,更安全易维护。
LambdaUpdateWrapper
- 继承自
AbstractLambdaWrapper,是UpdateWrapper的Lambda 版本。 - 同样用 Lambda 引用字段,同时支持设置更新字段(如
set(User::getEmail, "new@xxx.com"))。
1.1 核心设计思路
条件构造器的核心是「抽象分层 + 功能细分 + 语法糖(Lambda)」:
- 抽象层:
Wrapper(顶层)→AbstractWrapper(核心实现),定义所有条件方法(eq/like/gt 等); - 功能层:按「查询 / 更新」拆分
QueryWrapper/UpdateWrapper,聚焦不同 SQL 操作; - 语法糖层:Lambda 版本(
LambdaQueryWrapper/LambdaUpdateWrapper),用实体类属性引用替代硬写字段名,实现编译期字段校验。
1.2 核心能力:条件常用方法
所有条件构造器的核心是「条件方法」,以下是高频使用的方法(以 QueryWrapper 为例):
| 方法名 | 作用 | SQL 等价写法 | 示例 |
|---|---|---|---|
eq(column, val) |
等于 | WHERE column = val |
eq("age", 18) → age = 18 |
ne(column, val) |
不等于 | WHERE column != val |
ne("name", "张三") → name != '张三' |
gt(column, val) |
大于 | WHERE column > val |
gt("salary", 5000) → salary > 5000 |
ge(column, val) |
大于等于 | WHERE column >= val |
ge("age", 18) → age >= 18 |
lt(column, val) |
小于 | WHERE column < val |
lt("age", 30) → age < 30 |
le(column, val) |
小于等于 | WHERE column <= val |
le("salary", 20000) → salary <= 20000 |
like(column, val) |
模糊查询(包含) | WHERE column LIKE '%val%' |
like("name", "张") → name LIKE '%张%' |
likeLeft(column, val) |
左模糊 | WHERE column LIKE '%val' |
likeLeft("phone", "138") → phone LIKE '%138' |
likeRight(column, val) |
右模糊 | WHERE column LIKE 'val%' |
likeRight("email", "zhangsan") → email LIKE 'zhangsan%' |
isNull(column) |
字段为空 | WHERE column IS NULL |
isNull("avatar") → avatar IS NULL |
isNotNull(column) |
字段不为空 | WHERE column IS NOT NULL |
isNotNull("email") → email IS NOT NULL |
in(column, coll) |
IN 条件 | WHERE column IN (v1,v2) |
in("id", Arrays.asList(1,2,3)) → id IN (1,2,3) |
notIn(column, coll) |
NOT IN 条件 | WHERE column NOT IN (v1,v2) |
notIn("age", 18,20) → age NOT IN (18,20) |
between(column, v1, v2) |
区间条件 | WHERE column BETWEEN v1 AND v2 |
between("age", 18, 30) → age BETWEEN 18 AND 30 |
notBetween(column, v1, v2) |
非区间条件 | WHERE column NOT BETWEEN v1 AND v2 |
notBetween("salary", 5000, 10000) |
and(consumer) |
拼接 AND 条件 | WHERE ... AND (...) |
and(w -> w.eq("sex", 1).gt("age", 20)) → AND (sex=1 AND age>20) |
or(consumer) |
拼接 OR 条件 | WHERE ... OR (...) |
or(w -> w.eq("status", 0).eq("del_flag", 1)) → OR (status=0 AND del_flag=1) |
orderByAsc(column...) |
升序排序 | ORDER BY column ASC |
orderByAsc("age", "id") → ORDER BY age ASC, id ASC |
orderByDesc(column...) |
降序排序 | ORDER BY column DESC |
orderByDesc("create_time") → ORDER BY create_time DESC |
select(column...) |
指定查询字段 | SELECT column1, column2 |
select("id", "name", "age") → 只查这 3 个字段 |
如果使用LambdaQueryWrapper的话就要把**"age"改为User::age**,类似的其他的字段也是这样。
1.3 LambdaQueryWrapper与QueryWrapper核心原理差异
LambdaQueryWrapper与QueryWrapper核心原理差异在于字段解析机制:
QueryWrapper直接接收开发者传入的字符串型字段名,框架仅做简单拼接,既不校验字段合法性(需开发者自行保证与数据库 / 实体映射一致),也不处理字段名与实体属性的关联,字段错误仅在 SQL 执行阶段(运行时)因数据库字段不存在暴露;LambdaQueryWrapper基于 Java Lambda 表达式的「方法引用(Method Reference)」特性,解析User::getName这类 Lambda 表达式时,通过反射获取实体类的属性元信息(如属性名、@TableField注解),再映射为数据库字段名,该解析过程在编译后、运行前完成,若属性名写错(如User::getNme)会直接触发编译报错,同时能自动适配@TableField指定的数据库字段名(如实体属性name对应数据库user_name),从底层规避了字段名硬编码的风险。
补充核心底层逻辑:
QueryWrapper的字段处理逻辑:入参字符串 → 直接拼接至 SQL 片段 → 无任何校验 / 转换;LambdaQueryWrapper的字段处理逻辑:Lambda 表达式 → 反射解析SerializedLambda获取属性信息 → 结合TableInfoHelper(MP 元数据工具)匹配数据库字段名 → 生成正确 SQL 片段,全程依托 Java 反射和 MP 的实体元数据缓存,实现字段名的 “类型安全映射”。
1.4 常用方法举例说明
LambdaQueryWrapper<User> lambdaQuery = new LambdaQueryWrapper<User>()
.eq(User::getAge, 18) // 等于:age = 18
.ne(User::getName, "张三") // 不等于:name != '张三'
.gt(User::getSalary, 5000) // 大于:salary > 5000
.ge(User::getAge, 18) // 大于等于:age >= 18
.lt(User::getAge, 30) // 小于:age < 30
.le(User::getSalary, 20000) // 小于等于:salary <= 20000
.like(User::getName, "张") // 模糊查询(包含):name LIKE '%张%'
.likeLeft(User::getPhone, "138") // 左模糊:phone LIKE '%138'
.likeRight(User::getEmail, "zhangsan") // 右模糊:email LIKE 'zhangsan%'
.isNull(User::getAvatar) // 字段为空:avatar IS NULL
.isNotNull(User::getEmail) // 字段不为空:email IS NOT NULL
.in(User::getId, Arrays.asList(1,2,3)) // IN条件:id IN (1,2,3)
.notIn(User::getAge, 18,20) // NOT IN条件:age NOT IN (18,20)
.between(User::getAge, 18, 30) // 区间条件:age BETWEEN 18 AND 30
.notBetween(User::getSalary, 5000, 10000) // 非区间:salary NOT BETWEEN 5000 AND 10000
.and(w -> w.eq(User::getSex, 1).gt(User::getAge, 20)) // AND嵌套:AND (sex=1 AND age>20)
.or(w -> w.eq(User::getStatus, 0).eq(User::getDelFlag, 1)) // OR嵌套:OR (status=0 AND del_flag=1)
.orderByAsc(User::getAge, User::getId) // 升序:ORDER BY age ASC, id ASC
.orderByDesc(User::getCreateTime) // 降序:ORDER BY create_time DESC
.select(User::getId, User::getName, User::getAge); // 指定查询字段:SELECT id, name, age
1.5 空值处理
在实际业务中,前端传递的查询参数(如姓名、手机号、年龄等)经常会出现 null/ 空字符串的情况,如果直接用 eq/like 等方法拼接条件,会导致 SQL 出现 WHERE name = null 这类无效条件(数据库中 = null 永远查不到结果),因此空值过滤是条件构造器使用的核心必做步骤。
MP 条件构造器的所有条件方法都支持「条件前置校验」—— 第一个参数传入 boolean 类型的判断逻辑,只有当该逻辑为 true 时,才会拼接当前条件。
- 常用空值判断工具
推荐使用 org.apache.commons.lang3 下的 StringUtils/NumberUtils(需引入依赖),也可手写判断逻辑:
<!-- 引入 commons-lang3 依赖(SpringBoot 项目可直接用,无需额外引入) -->
<dependency>
<groupId>org.apache.commons.lang3</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.14.0</version>
</dependency>
| 数据类型 | 空值判断场景 | 工具类方法 | 手写逻辑示例 |
|---|---|---|---|
| 字符串 | 非空且非空白字符 | StringUtils.isNotBlank(str) |
str != null && !str.trim().isEmpty() |
| 数字 | 非 null | Objects.nonNull(num) |
num != null |
| 集合 | 非 null 且非空 | CollectionUtils.isNotEmpty(collection) |
coll != null && !coll.isEmpty() |
3. 实战示例(完整场景)
业务需求:根据前端传递的「姓名、手机号、年龄」查询用户,参数为空时不拼接对应条件。
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.CollectionUtils;
// 前端传入的查询参数(可能为null/空)
String name = null; // 姓名(空)
String phone = "138"; // 手机号(非空)
Integer age = 20; // 年龄(非空)
List<Long> roleIds = null; // 角色ID列表(空)
// 构建条件(自动过滤空值参数)
LambdaQueryWrapper<User> lambdaQuery = new LambdaQueryWrapper<User>()
// 姓名:非空且非空白时才拼接模糊查询
.like(StringUtils.isNotBlank(name), User::getName, name)
// 手机号:非空时拼接右模糊查询
.likeRight(StringUtils.isNotBlank(phone), User::getPhone, phone)
// 年龄:非null时拼接等于条件
.eq(Objects.nonNull(age), User::getAge, age)
// 角色ID:集合非空时拼接IN条件
.in(!CollectionUtils.isEmpty(roleIds), User::getRoleId, roleIds);
// 执行查询(最终SQL仅包含phone和age的条件)
List<User> userList = userMapper.selectList(lambdaQuery);
4. 简化封装(复用性优化)
如果项目中空值判断场景多,可封装工具类减少重复代码:
/**
* MP条件构造器空值处理工具类
*/
public class WrapperUtils {
// 字符串非空时拼接eq条件
public static <T> void eqIfNotBlank(LambdaQueryWrapper<T> wrapper, String val, SFunction<T, ?> column) {
if (StringUtils.isNotBlank(val)) {
wrapper.eq(column, val);
}
}
// 数字非null时拼接eq条件
public static <T> void eqIfNotNull(LambdaQueryWrapper<T> wrapper, Number val, SFunction<T, ?> column) {
if (Objects.nonNull(val)) {
wrapper.eq(column, val);
}
}
// 集合非空时拼接IN条件
public static <T> void inIfNotEmpty(LambdaQueryWrapper<T> wrapper, Collection<?> coll, SFunction<T, ?> column) {
if (!CollectionUtils.isEmpty(coll)) {
wrapper.in(column, coll);
}
}
}
// 调用示例(代码更简洁)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
WrapperUtils.eqIfNotBlank(wrapper, name, User::getName);
WrapperUtils.likeRightIfNotBlank(wrapper, phone, User::getPhone);
WrapperUtils.eqIfNotNull(wrapper, age, User::getAge);
5. 注意事项
-
避免直接使用
if嵌套:不要写if (xxx != null) { wrapper.eq(...) },会导致代码冗余,优先用条件构造器自带的「前置判断参数」; -
空字符串 vs null:
StringUtils.isNotBlank()会过滤空字符串("")和空白字符(" "),而StringUtils.isNotEmpty()仅过滤null和"",根据业务选择; -
数值类型:int/long 等基本类型不会为 null(默认 0),需提前处理(如前端传 0 时视为空):
// 年龄为0时视为空,不拼接条件 .eq(age != null && age != 0, User::getAge, age);
2. MP中自定义Wrapper拼接sql
MyBatis-Plus(MP)支持将自定义的注解 / XML 原生 SQL 与 Wrapper 动态条件结合使用,核心是把 Wrapper 生成的 WHERE 条件片段,注入到注解 / XML 编写的 SQL 骨架中,既保留原生 SQL 对多表关联、复杂函数等场景的适配能力,又复用 Wrapper 动态拼接条件的便捷性,具体逻辑可梳理为:
2.1 核心实现逻辑
- 编写 SQL 骨架:在
@Select/@Update等注解,或 Mapper.xml 的 SQL 标签中,编写固定的 SQL 部分(如 SELECT 字段、FROM 表、JOIN 关联等),并预留${ew.customSqlSegment}作为 WHERE 条件的占位符; - 声明 Wrapper 参数:在 Mapper 接口的自定义方法中,添加 Wrapper(或 LambdaQueryWrapper/LambdaUpdateWrapper)类型的入参,并通过
@Param(Constants.WRAPPER)注解指定参数名为 MP 内置的ew(必须对齐); - 动态注入条件:执行方法时,MP 会自动将 Wrapper 拼接好的 WHERE 条件片段(包含
WHERE关键字),替换到 SQL 骨架的${ew.customSqlSegment}占位符处,最终生成完整的可执行 SQL。
2.2 关键细节补充
- **(重要)**若仅需注入条件片段(不含
WHERE关键字),可将占位符替换为${ew.sqlSegment},但需手动在 SQL 中补充WHERE关键字; - Wrapper 生成的条件会自动做参数化处理,可避免 SQL 注入,自定义 SQL 部分仍需遵循
#{}占位符规范; - 该方式适配所有自定义 SQL 场景(查询 / 更新 / 删除),仅需对应调整注解 / XML 中的 SQL 类型(如
<update>/@Update)。
2.3 极简示例(注解式)
// Mapper接口
public interface UserMapper extends BaseMapper<User> {
@Select("SELECT u.id, u.name, r.role_name " +
"FROM t_user u LEFT JOIN t_role r ON u.role_id = r.id " +
"${ew.customSqlSegment}") // 注入Wrapper生成的WHERE条件
List<UserRoleVO> selectUserWithRole(@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper);
}
// 调用示例
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
.eq(User::getStatus, 1)
.like(User::getName, "张");
List<UserRoleVO> list = userMapper.selectUserWithRole(wrapper);
2.4 极简示例(xml式)
// Mapper接口
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.liu.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserMapper extends BaseMapper<User> {
// @Update("update user set age=age+50 ${ew.customSqlSegment}")
public int Update1(@Param("ew") Wrapper<User> wrapper);
}
//service接口
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.liu.entity.User;
public interface UserService extends IService<User> {
public int Update1(Wrapper<User> wrapper);
}
//serviceImpl实现类
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.liu.entity.User;
import com.liu.mapper.UserMapper;
import com.liu.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService {
UserMapper userMapper;
@Autowired
public UserServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public int Update1(Wrapper<User> wrapper) {
return userMapper.Update1(wrapper);
}
}
//调用示例
@Test
void aaa(){
LambdaUpdateWrapper<User> wrapper=new LambdaUpdateWrapper<>();
wrapper.eq(User::getPhone,"19562576496");
int i = userService.Update1(wrapper)+100;
System.out.println("==================hhh===================="+i);
}
<!--user.xml配置文件-->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.liu.mapper.UserMapper">
<update id="Update1">
update user set age=age-60 ${ew.customSqlSegment}
</update>
</mapper>
3. IService<T>
3.1 概述
IService 是 MP 封装的通用服务层接口,它基于 BaseMapper 做了更高层的封装,提供了批量操作、链式查询、分页查询等更贴合业务场景的方法,让你在 Service 层可以直接调用,大幅减少重复代码。
还是以User实体类为例,在写UserService接口的时候要继承IService接口,同时指定泛型为实体类:
public interface UserService extends IService<User> {
}
然后对应的实现类UserServiceImpl要继承ServiceImpl类(泛型里面写对应的Mapper接口和实体类,以便通过反射来找到对应的),并实现UserService接口:
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
为什么要这样做呢?
因为:ServiceImpl<M, T> 实现了 IService<T> 里的所有抽象方法,所以你的业务实现类一旦继承 ServiceImpl<M, T>,就相当于一次性拿到了整套通用 CRUD 的实现
3.2 **BaseMapper<T> 和 IService<T>**的区别
二者在“基础 CRUD”层面几乎一一映射,差异只体现在返回值类型、批量/分页/事务增强和链式 API 上。
BaseMapper有批量操作,但是种类少、功能裸、不带事务/分批”,跟 IService 的批量不是一回事。
| 维度 | BaseMapper | IService |
|---|---|---|
| 层级 | DAO 层(数据访问层) | Service 层(业务层) |
| 批量操作 | 基础批量(无分批 / 事务) | 增强批量(分批 / 带事务) |
| 链式调用 | 不支持 | 支持(lambdaQuery 等) |
| 适用场景 | 简单 CRUD、自定义 SQL | 业务层批量 / 复杂条件操作 |
下面我将讲述一下IService里面的批量操作:
3.3 批量操作
MyBatis-Plus 的IService提供了比BaseMapper更强大的批量操作能力,不用自己写循环或批量 SQL,直接调用封装好的方法即可,效率和代码简洁度都更高。
3.3.1 IService 批量操作的核心方法(对比BaseMapper)
BaseMapper只有单条操作(如insert),而IService直接提供批量方法:
| 操作类型 | IService 方法(批量) |
BaseMapper 对应(无批量,需自己循环) |
|---|---|---|
| 新增 | saveBatch(list, batchSize) |
循环调用insert |
| 修改 | updateBatchById(list, batchSize) |
循环调用updateById |
| 逻辑删除 | removeByIds(ids) |
循环调用deleteById |
3.3.2 IService 批量操作实战示例
以下用UserService演示常用批量操作(基于之前的User实体):
- 批量新增(
saveBatch)
// Controller中调用
@PostMapping("/user/batch/save")
public boolean batchSaveUser(@RequestBody List<User> userList) {
// 第二个参数:每批插入的数量(推荐设置,避免SQL过长)
return userService.saveBatch(userList, 20); // 每20条分一批插入
}
- 批量更新(
updateBatchById)
@PostMapping("/user/batch/update")
public boolean batchUpdateUser(@RequestBody List<User> userList) {
// 要求List中的User必须包含主键id(否则无法定位更新)
return userService.updateBatchById(userList, 15); // 每15条分一批更新
}
- 批量删除(
removeByIds)
@PostMapping("/user/batch/remove")
public boolean batchRemoveUser(@RequestBody List<Long> idList) {
// 根据主键id批量删除
return userService.removeByIds(idList);
}
- 批量查询(
listByIds)
@GetMapping("/user/batch/get")
public List<User> batchGetUser(@RequestParam List<Long> idList) {
// 根据主键id批量查询
return userService.listByIds(idList);
}
3.4 高级批量操作(链式 + 批量)
IService支持链式调用,可以结合条件实现更灵活的批量操作:
// 批量更新年龄>20的用户,将密码改为"123456"
@PostMapping("/user/batch/update/condition")
public boolean batchUpdateByCondition() {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<User>()
.gt(User::getAge, 20) // 条件:年龄>20
.set(User::getPassword, "123456"); // 更新字段
return userService.update(wrapper);
}
// 链式查询
List<User> list = userService.lambdaQuery()
.eq(User::getAge, 18)
.like(User::getName, "张")
.list();
// 链式更新
boolean update = userService.lambdaUpdate()
.eq(User::getId, 1L)
.set(User::getAge, 19)
.update();
3.5 count计数
业务中常用 userService.count(lambdaQuery.eq(User::getSex, 1)),比自己查 list 再 size 更高效。
3.5 注意事项
-
批量大小(
batchSize):建议根据数据库性能设置(比如 MySQL 默认
max_allowed_packet限制 SQL 长度,一般设置 10-50 条 / 批)。 -
主键要求:
批量更新 / 删除 / 查询必须基于主键 id(
IService的批量方法依赖主键定位数据)。 -
效率对比:
IService的批量方法是单 SQL 批量执行(如INSERT INTO ... VALUES (),(),()),比BaseMapper循环单条操作效率高 10 倍以上。
4. 逻辑删除
4.1 什么是逻辑删除?
逻辑删除(Logical Delete)是伪删除:不物理删除数据库中的记录(不执行 DELETE 语句),而是通过标记字段(如 deleted/is_deleted)标识记录 “是否被删除”,保留数据的完整性和可追溯性。
对比物理删除:
- 物理删除:
DELETE FROM user WHERE id = 1→ 记录从数据库中永久消失; - 逻辑删除:
UPDATE user SET deleted = 1 WHERE id = 1→ 记录仍存在,仅deleted字段标记为 “已删除”。
4.2 MyBatis-Plus 如何实现逻辑删除?
MP 对逻辑删除做了全自动封装,无需手动写 UPDATE 语句,核心逻辑:
-
执行
deleteById(id)时,MP 自动转为UPDATE语句(更新逻辑删除字段); -
执行
selectList/selectById等查询时,MP 自动拼接WHERE deleted = 未删除值,过滤已删除记录; -
支持全局配置,无需在每个实体类重复注解。
4.3 MP 逻辑删除的配置步骤(完整示例)
1.以User表为例:先在表中新增 deleted 字段(类型:tinyint/int,默认值 0),可以直接改变表中添加,也可执行以下语句:
ALTER TABLE `user` ADD COLUMN `deleted` TINYINT(1) DEFAULT 0 COMMENT '逻辑删除标识:0=未删除,1=已删除';
2.在User实体类加入deleted属性(仅展示部分代码):
public class User {
@TableLogic(value = "0",delval = "1")
private Integer deleted;
}
value:逻辑未删除值(对应全局的logic-not-delete-value);delval:逻辑删除值(对应全局的logic-delete-value)。
下面讲述全局配置逻辑删除(推荐,无需实体类重复注解)
在 application.yml 中配置 MP 全局逻辑删除规则:
mybatis-plus:
global-config:
db-config:
# 全局逻辑删除字段名(对应数据库字段名)
logic-delete-field: deleted
# 逻辑删除值(已删除):数据库中存储的标记值
logic-delete-value: 1
# 逻辑未删除值(默认):数据库中存储的标记值
logic-not-delete-value: 0
4.4 核心用法与自动行为
- 删除操作:自动转为更新
// 调用 MP 自带的 deleteById 方法
userMapper.deleteById(1L);
// MP 自动生成的 SQL(而非 DELETE):
// UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
- 查询操作:自动过滤已删除记录
// 调用 MP 自带的 selectById/selectList 等方法
User user = userMapper.selectById(1L);
List<User> userList = userMapper.selectList(null);
// MP 自动拼接 WHERE 条件,过滤 deleted=1 的记录:
// SELECT id, user_name, age, deleted FROM user WHERE id = 1 AND deleted = 0
// SELECT id, user_name, age, deleted FROM user WHERE deleted = 0
- 如需查询已删除记录(手动关闭过滤)
通过 QueryWrapper 手动指定逻辑删除字段值,覆盖 MP 的自动过滤:
List<User> allUser = userMapper.selectList(
new QueryWrapper<User>().eq("deleted", 1) // 查询已删除的记录
);
// 生成的 SQL:SELECT id, user_name, age, deleted FROM user WHERE deleted = 1
- 恢复已删除记录(手动更新)
逻辑删除的记录可通过更新字段恢复:
User user = new User();
user.setId(1L);
user.setDeleted(0); // 恢复为未删除
userMapper.updateById(user);
// 生成的 SQL:UPDATE user SET deleted = 0 WHERE id = 1
4.5 注意事项
-
逻辑删除仅对 MP 自带的delete/select方法生效,自定义 SQL 需手动处理
<!-- 自定义 SQL 需手动加 deleted 条件 --> <select id="selectByName" resultType="com.liu.entity.User"> SELECT id, user_name, age FROM user WHERE user_name = #{name} AND deleted = 0 </select> -
逻辑删除字段建议设置默认值(如
0),避免新增记录时字段为null; -
若需彻底删除数据,可手动执行
DELETE语句(不推荐,除非确认无需追溯); -
分页查询、条件查询均会自动过滤已删除记录,无需手动拼接条件。
4.6 总结
核心价值:
- 无需手动写更新语句,降低开发成本;
- 自动过滤已删除记录,避免业务代码重复判断;
- 保留数据完整性,支持数据恢复;
- 全局配置 + 局部覆盖,适配不同实体类的规则。
使用时只需记住:配置逻辑删除字段 → 调用 MP 原生 CRUD 方法 → 自动实现伪删除 + 过滤,自定义 SQL 需手动处理逻辑删除条件。
5. 乐观锁
5.1 什么是乐观锁?
在 MyBatis-Plus(MP)中,乐观锁用于解决并发更新冲突(多人同时修改同一条数据时,避免 “覆盖更新”),核心是通过版本号机制实现。以下是乐观锁的完整用法步骤(结合代码示例):
5.2 MyBatis-Plus 如何实现乐观锁?
1.表中新增 version 字段(类型:int/tinyint,默认值 1)或者用以下语句来实现:
ALTER TABLE `user` ADD COLUMN `version` INT(10) DEFAULT 1 COMMENT '乐观锁版本号';
2.配置乐观锁插件
@Configuration //告诉 Spring 这是一个配置类,会自动扫描并加载其中的 @Bean。
public class MpConfig {
@Bean //将 MybatisPlusInterceptor 对象注册为 Spring Bean,MP 会自动识别并启用这个拦截器。
public MybatisPlusInterceptor mpInterceptor() {
// 1. 定义 MP 拦截器容器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
// 2. 添加分页插件(分页用)
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
// 3. 添加乐观锁插件
mpInterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return mpInterceptor;
}
}
3.实体类标记版本字段,在实体类中用 @Version 注解标记版本字段:
public class User {
@Version
private Integer version;
}
5.3 使用示例:
场景:多人同时修改同一条用户数据
@Service
public class UserServiceImpl {
@Autowired
private UserMapper userMapper;
// 乐观锁更新示例
public boolean updateUserWithOptimisticLock(Long userId, String newName) {
// 1. 查询数据,获取当前版本号
User user = userMapper.selectById(userId);
if (user == null) {
return false;
}
// 2. 修改业务字段
user.setName(newName);
// 3. MP 自动拼接版本号条件,执行更新
int affectedRows = userMapper.updateById(user);
// 4. 判断更新是否成功:affectedRows=1 成功,=0 失败(版本号不匹配)
return affectedRows > 0;
}
// 重试逻辑(对外提供的方法,最多重试3次)
public boolean updateWithRetry(Long userId, String newName) {
// 重试次数(可配置,比如放到配置文件)
int retryCount = 3;
// 循环重试
while (retryCount > 0) {
boolean success = updateUserWithOptimisticLock(userId, newName);
if (success) {
System.out.println("更新成功!");
return true;
}
// 重试次数-1
retryCount--;
System.out.println("剩余重试次数:" + retryCount);
// 短暂休眠(避免高频重试,给其他线程释放锁的时间)
try {
Thread.sleep(100); // 100毫秒,可根据业务调整
} catch (InterruptedException e) {
// 中断异常处理:恢复线程中断状态
Thread.currentThread().interrupt();
System.out.println("重试休眠被中断,停止重试");
return false;
}
}
// 所有重试都失败
System.out.println("重试3次后仍失败,更新终止");
return false;
}
}
执行效果(SQL 层面):
-
初始数据:
id=1, name="张三", version=1 -
执行
updateUserWithOptimisticLock(1, "李四")后,MP 生成的 SQL:UPDATE user SET name = '李四', version = 2 WHERE id = 1 AND version = 1 -
若此时有另一个线程同时修改该数据,其版本号仍为
1,则更新时会因version=1不匹配,返回affectedRows=0,更新失败。
在Controller中调用:
@RestController
public class UserController {
@Resource //对于新手来说和 @Autowired 都友好
private UserServiceImpl userServiceImpl;
@PostMapping("/user/update")
public String updateUser(
@RequestParam Long userId,
@RequestParam String newName) {
boolean success = userService.updateWithRetry(userId, newName);
if (success) {
return "更新成功";
} else {
// 友好提示用户
return "数据已被其他用户修改,请刷新页面后重试";
}
}
}
5.4 注意事项
-
版本字段必须用包装类:避免默认值
0导致更新失败(如private Integer version;,而非private int version;); -
仅对 MP 自带方法生效:自定义 SQL 需手动处理版本号条件;
-
乐观锁 vs 悲观锁
:
- 乐观锁:适合并发冲突少的场景(无锁,性能高);
- 悲观锁:适合并发冲突多的场景(用
FOR UPDATE加锁,性能低);
-
版本号不回滚:若更新失败,版本号不会回滚(需重新查询最新版本号)。
5.5 总结
乐观锁的核心流程:实体类加 @Version → 配置乐观锁插件 → 查询数据获取版本号 → 更新时MP自动处理版本号 → 业务层处理冲突重试。
这是 MP 中解决并发更新的常用方案,配置简单且性能友好。
3. 扩展功能
1. MP分页插件
MP 分页是对 MyBatis 原生分页的轻量化封装,核心目标是简化分页开发,无需手写 LIMIT 语句、手动统计总条数,仅通过少量配置 + API 调用,即可快速实现分页查询,是后端列表查询场景的高频刚需功能。
使用流程
Spring Boot 中通过配置类注册分页拦截器(必须配置,不配置不起作用),指定数据库类型(也可不指定):
@Configuration //告诉 Spring 这是一个配置类,会自动扫描并加载其中的 @Bean。
public class MybatisConfig {
@Bean //将 MybatisPlusInterceptor 对象注册为 Spring Bean,MP 会自动识别并启用这个拦截器。
public MybatisPlusInterceptor mpInterceptor() {
// 1. 定义 MP 拦截器容器
MybatisPlusInterceptor mpInterceptor = new MybatisPlusInterceptor();
// 2. 添加分页插件(分页用)
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
接下来用一个测试方法来测试:
@Test
public void test() {
// 1. 构建分页条件:第1页,每页2条
Page<User> page = new Page<>(1,2);
// 2.构建排序规则,先按照age降序(desc)排序,如果age相同的话,就按照id(asc)升序查询
page.addOrder(OrderItem.desc("age"),OrderItem.asc("id"));
userMapper.selectPage(page,null);// 第二个参数是查询条件(Wrapper),null表示无条件
System.out.println("Size="+page.getSize());//每页几条
System.out.println("Pages="+page.getPages());//总页数
System.out.println("Total="+page.getTotal());//总条数
System.out.println("Current="+page.getCurrent());//当前页
boolean hasNext = resultPage.hasNext();//判断是否有下一页(true表示有,flase表示无)
for (User record : page.getRecords()) { //输出当前页的查询的信息
System.out.println(record);
}
}
简言之,MP 分页的核心价值是 “把分页的重复工作交给框架,开发者只需关注业务条件”,是后端开发中 “低成本高收益” 的基础技能。
2.自定义 SQL 分页
MP 自带的 selectPage 仅适配单表分页,实际开发中常遇到多表联查、自定义复杂 SQL、聚合函数统计等场景,此时需要基于 MP 分页机制实现「自定义分页」,核心是复用 MP 的分页拦截器和 Page 封装,仅需改造 SQL 编写方式。
一共分为注解式和XML式,下面简单讲解一下注解式,XML式与之类似再次就不过多赘述:
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
// 自定义VO:封装用户+角色信息
public class UserRoleVO {
private Long id;
private String userName;
private String roleName;
// 省略getter/setter
}
// UserMapper 接口
public interface UserMapper extends BaseMapper<User> {
/**
* 自定义分页:查询用户+关联角色信息
* @param page 分页条件(页码、页大小)
* @param roleId 筛选条件(可选)
* @return 分页结果(含总条数、当前页数据)
*/
@Select("SELECT u.id, u.name AS userName, r.role_name AS roleName " +
"FROM user u LEFT JOIN role r ON u.role_id = r.id " +
"WHERE r.id = #{roleId}") // 自定义多表联查SQL
IPage<UserRoleVO> selectUserRolePage(
Page<UserRoleVO> page, // 分页参数(MP自动处理分页)
@Param("roleId") Long roleId // 业务筛选参数
);
}
// Service 层
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
public IPage<UserRoleVO> getUserRolePage(Integer pageNum, Integer pageSize, Long roleId) {
// 1. 构建分页对象:第pageNum页,每页pageSize条
Page<UserRoleVO> page = new Page<>(pageNum, pageSize);
// 2. 调用自定义分页方法
return baseMapper.selectUserRolePage(page, roleId);
}
}
// Controller 层
@GetMapping("/user/role/page")
public IPage<UserRoleVO> getUserRolePage(
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam Long roleId) {
return userService.getUserRolePage(pageNum, pageSize, roleId);
}
关键说明
Page<T>是 MP 分页核心类,构造时传入current(当前页)、size(页大小);- 返回值
IPage<T>是Page<T>的接口,包含分页核心属性:getRecords():当前页数据列表;getTotal():总条数(MP 自动执行COUNT统计);getPages():总页数;getCurrent()/getSize():当前页 / 页大小。
总结
MP 自定义分页的本质是:复用 MP 的 Page 封装 + 分页拦截器,仅替换查询 SQL 为自定义逻辑,核心流程:
- 定义
Page<T>分页对象(指定页码 / 页大小); - 编写自定义 SQL(注解 / XML),无需手写 LIMIT;
- 调用 Mapper 方法,MP 自动注入分页条件;
- 复杂场景手动统计总条数,保证分页准确性。
这种方式既保留了 MP 分页的便捷性,又适配了实际业务中复杂的多表 / 聚合查询场景,是后端分页开发的核心方案。
4. 总结
MyBatis-Plus 就是 MyBatis 的增强工具,不用改原生用法,还能大幅省代码、提效率。不管是 SpringBoot、普通 Spring 还是非 Spring 环境,引入对应依赖就能用,核心靠 BaseMapper 和 IService 封装了通用 CRUD,不用自己写基础 SQL。
条件构造器能替代手写 SQL 条件,Lambda 版本还能避免字段名写错;自定义 Wrapper 能把动态条件和自定义 SQL 结合,多表查询也方便。批量操作比手动循环效率高多了,逻辑删除、乐观锁都是开箱即用,不用自己写复杂逻辑,分页插件配一下就能快速实现分页查询。
整体来说,MP 把开发中重复的活儿都包了,简单配置就能搞定单表 CRUD、并发更新、分页这些常见需求,性价比很高,是 Java 后端开发的实用工具。
更多推荐



所有评论(0)