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,无属性。
  • 成员变量名与数据库关键字冲突。
  • 成员变量不是数据库字段。
属性名 类型 默认值 作用说明
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 注入风险。

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

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

LambdaQueryWrapperQueryWrapper核心原理差异在于字段解析机制

  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()
数字 非 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. 注意事项

  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 的批量不是一回事。

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

  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 条 / 批)。

  • 主键要求:

    批量更新 / 删除 / 查询必须基于主键 idIService的批量方法依赖主键定位数据)。

  • 效率对比:

    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编程工具,助力开发者即刻编程。

更多推荐