Mybatis-plus

MyBatis-Plus(简称MP)是MyBatis的强力增强工具,在保留MyBatis所有特性的基础上,旨在简化开发、提升效率。它不是一个全新的框架,而是一个“站在巨人肩膀上”的国产开源组件。

1. 快速入门

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 配置文件中手动配置。

  1. 非 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,无属性。
  • 成员变量名与数据库关键字冲突。
  • 成员变量不是数据库字段。
属性名类型默认值作用说明
valueString“”指定数据库表中对应的字段名(解决字段名不一致)
existbooleantrue标记该字段是否为数据库表中的真实字段(false = 非数据库字段)
fillFieldFillFieldFill.DEFAULT字段自动填充策略(如创建时间、更新时间自动赋值)
selectbooleantrue是否参与查询(false = 查询时忽略该字段)
updateString“”自定义更新语句的字段表达式(如update="%s+1"实现字段自增)
insertStrategyFieldStrategyDEFAULT插入时的字段验证策略(如忽略空值)
updateStrategyFieldStrategyDEFAULT更新时的字段验证策略
whereStrategyFieldStrategyDEFAULT拼接 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 注入风险。

下面是条件构造器的多个类:

image-20251221112034547
  1. Wrapper
  • 最顶层的抽象基类,定义了所有条件构造器的通用方法(如eq等于、like模糊查询等)。
  • 不能直接实例化,仅作为父类提供基础能力。
  1. AbstractWrapper
  • 继承自Wrapper的抽象子类,是条件构造的核心实现类,不能直接实例化使用。
  • 封装了 SQL 的WHERE子句逻辑(如条件拼接、逻辑运算符and/or等),是QueryWrapper、UpdateWrapper的直接父类。
  1. QueryWrapper
  • 继承自AbstractWrapper,专门用于构建查询 / 删除的条件(对应 SQL 的SELECT/DELETE操作)。
  • 支持链式调用添加条件(如eq("name", "张三").gt("age", 18)),也可指定查询字段(select("id", "name"))。
  1. UpdateWrapper
  • 继承自AbstractWrapper,专门用于构建更新的条件(对应 SQL 的UPDATE操作)。
  • 除了条件,还支持直接设置更新字段(如set("email", "new@xxx.com")),可同时写SET和WHERE子句。
  1. AbstractLambdaWrapper
  • 继承自AbstractWrapper的抽象子类,提供Lambda 表达式支持的基础能力,不能直接实例化使用。
  • 作用是让条件构造时可以通过 “实体类::属性” 的方式引用字段(避免硬写字段名字符串)。
  1. LambdaQueryWrapper
  • 继承自AbstractLambdaWrapper,是QueryWrapper的Lambda 版本。
  • 用 Lambda 表达式引用实体类属性(如eq(User::getName, "张三")),编译时就能检查字段名是否正确,更安全易维护。
  1. LambdaUpdateWrapper
  • 继承自AbstractLambdaWrapper,是UpdateWrapper的Lambda 版本。
  • 同样用 Lambda 引用字段,同时支持设置更新字段(如set(User::getEmail, "new@xxx.com"))。
1.1 核心设计思路

条件构造器的核心是「抽象分层 + 功能细分 + 语法糖(Lambda)」:

  1. 抽象层:Wrapper(顶层)→ AbstractWrapper(核心实现),定义所有条件方法(eq/like/gt 等);
  2. 功能层:按「查询 / 更新」拆分 QueryWrapper/UpdateWrapper,聚焦不同 SQL 操作;
  3. 语法糖层:Lambda 版本(LambdaQueryWrapper/LambdaUpdateWrapper),用实体类属性引用替代硬写字段名,实现编译期字段校验。
1.2 核心能力:条件常用方法

所有条件构造器的核心是「条件方法」,以下是高频使用的方法(以 QueryWrapper 为例):

方法名作用SQL 等价写法示例
eq(column, val)等于WHERE column = valeq("age", 18) → age = 18
ne(column, val)不等于WHERE column != valne("name", "张三") → name != '张三'
gt(column, val)大于WHERE column > valgt("salary", 5000) → salary > 5000
ge(column, val)大于等于WHERE column >= valge("age", 18) → age >= 18
lt(column, val)小于WHERE column < vallt("age", 30) → age < 30
le(column, val)小于等于WHERE column <= valle("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 NULLisNull("avatar") → avatar IS NULL
isNotNull(column)字段不为空WHERE column IS NOT NULLisNotNull("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 v2between("age", 18, 30) → age BETWEEN 18 AND 30
notBetween(column, v1, v2)非区间条件WHERE column NOT BETWEEN v1 AND v2notBetween("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 ASCorderByAsc("age", "id") → ORDER BY age ASC, id ASC
orderByDesc(column...)降序排序ORDER BY column DESCorderByDesc("create_time") → ORDER BY create_time DESC
select(column...)指定查询字段SELECT column1, column2select("id", "name", "age") → 只查这 3 个字段

如果使用LambdaQueryWrapper的话就要把**"age"改为User::age**,类似的其他的字段也是这样。

1.3 LambdaQueryWrapper与QueryWrapper核心原理差异

LambdaQueryWrapper与QueryWrapper核心原理差异在于字段解析机制:

  1. QueryWrapper直接接收开发者传入的字符串型字段名,框架仅做简单拼接,既不校验字段合法性(需开发者自行保证与数据库 / 实体映射一致),也不处理字段名与实体属性的关联,字段错误仅在 SQL 执行阶段(运行时)因数据库字段不存在暴露;
  2. 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 时,才会拼接当前条件。

  1. 常用空值判断工具

推荐使用 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()
数字非 nullObjects.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. 注意事项

  1. 避免直接使用 if 嵌套:不要写 if (xxx != null) { wrapper.eq(...) },会导致代码冗余,优先用条件构造器自带的「前置判断参数」;

  2. 空字符串 vs null:StringUtils.isNotBlank() 会过滤空字符串("")和空白字符(" "),而 StringUtils.isNotEmpty() 仅过滤 null 和 "",根据业务选择;

  3. 数值类型: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 核心实现逻辑
  1. 编写 SQL 骨架:在 @Select/@Update 等注解,或 Mapper.xml 的 SQL 标签中,编写固定的 SQL 部分(如 SELECT 字段、FROM 表、JOIN 关联等),并预留 ${ew.customSqlSegment} 作为 WHERE 条件的占位符;
  2. 声明 Wrapper 参数:在 Mapper 接口的自定义方法中,添加 Wrapper(或 LambdaQueryWrapper/LambdaUpdateWrapper)类型的入参,并通过 @Param(Constants.WRAPPER) 注解指定参数名为 MP 内置的 ew(必须对齐);
  3. 动态注入条件:执行方法时,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 的批量不是一回事。

维度BaseMapperIService
层级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实体):

  1. 批量新增(saveBatch)
// Controller中调用
@PostMapping("/user/batch/save")
public boolean batchSaveUser(@RequestBody List<User> userList) {
    // 第二个参数:每批插入的数量(推荐设置,避免SQL过长)
    return userService.saveBatch(userList, 20); // 每20条分一批插入
}
  1. 批量更新(updateBatchById)
@PostMapping("/user/batch/update")
public boolean batchUpdateUser(@RequestBody List<User> userList) {
    // 要求List中的User必须包含主键id(否则无法定位更新)
    return userService.updateBatchById(userList, 15); // 每15条分一批更新
}
  1. 批量删除(removeByIds)
@PostMapping("/user/batch/remove")
public boolean batchRemoveUser(@RequestBody List<Long> idList) {
    // 根据主键id批量删除
    return userService.removeByIds(idList);
}
  1. 批量查询(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 语句,核心逻辑:

  1. 执行 deleteById(id) 时,MP 自动转为 UPDATE 语句(更新逻辑删除字段);

  2. 执行 selectList/selectById 等查询时,MP 自动拼接 WHERE deleted = 未删除值,过滤已删除记录;

  3. 支持全局配置,无需在每个实体类重复注解。

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 核心用法与自动行为
  1. 删除操作:自动转为更新
// 调用 MP 自带的 deleteById 方法
userMapper.deleteById(1L);

// MP 自动生成的 SQL(而非 DELETE):
// UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0
  1. 查询操作:自动过滤已删除记录
// 调用 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
  1. 如需查询已删除记录(手动关闭过滤)

通过 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
  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 总结

核心价值:

  1. 无需手动写更新语句,降低开发成本;
  2. 自动过滤已删除记录,避免业务代码重复判断;
  3. 保留数据完整性,支持数据恢复;
  4. 全局配置 + 局部覆盖,适配不同实体类的规则。

使用时只需记住:配置逻辑删除字段 → 调用 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 注意事项
  1. 版本字段必须用包装类:避免默认值 0 导致更新失败(如 private Integer version;,而非 private int version;);

  2. 仅对 MP 自带方法生效:自定义 SQL 需手动处理版本号条件;

  3. 乐观锁 vs 悲观锁

    :

    • 乐观锁:适合并发冲突少的场景(无锁,性能高);
    • 悲观锁:适合并发冲突多的场景(用 FOR UPDATE 加锁,性能低);
  4. 版本号不回滚:若更新失败,版本号不会回滚(需重新查询最新版本号)。

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 为自定义逻辑,核心流程:

  1. 定义 Page<T> 分页对象(指定页码 / 页大小);
  2. 编写自定义 SQL(注解 / XML),无需手写 LIMIT;
  3. 调用 Mapper 方法,MP 自动注入分页条件;
  4. 复杂场景手动统计总条数,保证分页准确性。

这种方式既保留了 MP 分页的便捷性,又适配了实际业务中复杂的多表 / 聚合查询场景,是后端分页开发的核心方案。

4. 总结

MyBatis-Plus 就是 MyBatis 的增强工具,不用改原生用法,还能大幅省代码、提效率。不管是 SpringBoot、普通 Spring 还是非 Spring 环境,引入对应依赖就能用,核心靠 BaseMapper 和 IService 封装了通用 CRUD,不用自己写基础 SQL。

条件构造器能替代手写 SQL 条件,Lambda 版本还能避免字段名写错;自定义 Wrapper 能把动态条件和自定义 SQL 结合,多表查询也方便。批量操作比手动循环效率高多了,逻辑删除、乐观锁都是开箱即用,不用自己写复杂逻辑,分页插件配一下就能快速实现分页查询。

整体来说,MP 把开发中重复的活儿都包了,简单配置就能搞定单表 CRUD、并发更新、分页这些常见需求,性价比很高,是 Java 后端开发的实用工具。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐