在持久层框架体系中,MyBatis 凭借灵活的 SQL 控制能力成为主流,但单表 CRUD 仍需手写大量 XML 代码;Hibernate 侧重全自动化 ORM,但灵活性不足。而 MyBatis-Plus(简称 MP) 作为 MyBatis 的增强工具,在保留 MyBatis 原有优势的基础上,实现了单表 CRUD 零 XML 开发,大幅提升开发效率。本文将从入门到实战,全面讲解 MP 的核心功能与落地技巧。
一、快速入门:5 分钟搞定单表 CRUD
1.1 入门案例:告别重复 XML 代码
先看一个痛点:传统 MyBatis 实现单表增删改查,需要在 XML 中编写大量固定格式的 SQL(如 UserMapper.xml)。而 MP 可以彻底简化这一过程,核心步骤仅两步:
步骤 1:引入 MyBatis-Plus 起步依赖
MP 提供了官方 starter,集成 MyBatis 核心功能并实现自动装配,直接替换原有 MyBatis 依赖即可:
xml
<!-- 移除原有 MyBatis starter --> <!-- <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> --> <!-- 引入 MyBatis-Plus starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency>
步骤 2:继承 BaseMapper 接口
自定义 Mapper 接口继承 MP 提供的 BaseMapper,并指定实体类泛型,即可直接拥有单表 CRUD 能力:
java
运行
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.itheima.mp.domain.po.User; // 泛型指定对应实体类 public interface UserMapper extends BaseMapper<User> { // 无需手写任何方法,BaseMapper 已内置增删改查 }
快速替换原有 CRUD 代码
继承 BaseMapper 后,可直接调用内置方法实现以下功能,无需编写 XML:
| 功能 | MP 调用方法 |
| 新增用户 | userMapper.insert(user) |
| 根据 ID 查询用户 | userMapper.selectById(1L) |
| 批量查询(ID 列表) | userMapper.selectBatchIds(Arrays.asList(1L,2L)) |
| 根据 ID 更新用户 | userMapper.updateById(user) |
| 根据 ID 删除用户 | userMapper.deleteById(1L) |
改造完成后,原 UserMapper_20231023_150307.xml 中的 SQL 代码可全部删除,代码量大幅减少!
1.2 核心注解:解决表与实体的映射问题
MP 基于反射自动映射实体类与数据库表,但遇到「表名不一致、字段名不匹配」等场景时,需通过注解手动指定映射关系:
| 注解 | 作用 | 核心属性 |
@TableName |
指定实体类对应的数据表名 | value:表名 |
@TableId |
指定主键字段 | type:主键生成策略(如雪花算法) |
@TableField |
指定普通字段 | value:字段名;exist:是否为数据库字段(false 表示非数据库字段) |
示例:实体类映射配置
java
运行
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; @Data // 实体类名与表名不一致时指定 @TableName("tb_user") public class User { // 主键,使用雪花算法生成 @TableId(type = IdType.ASSIGN_ID) private Long id; // 字段名一致,可省略 @TableField private String username; // 字段名不一致(实体:userInfo → 数据库:info) @TableField("info") private String userInfo; // 非数据库字段,查询时忽略 @TableField(exist = false) private String temp; }
主键生成策略(IdType)说明
AUTO:数据库自增(需表主键设置自增);INPUT:手动输入主键值;ASSIGN_ID:MP 内置雪花算法生成 Long 类型主键(推荐);ASSIGN_UUID:生成 UUID 主键。
1.3 核心配置:自定义 MP 行为
MP 配置继承 MyBatis 原生配置,可在 application.yml 中自定义规则:
yaml
mybatis-plus: type-aliases-package: com.itheima.mp.domain.po # 实体类别名扫描包 mapper-locations: "classpath*:/mapper/**/*.xml" # Mapper XML 路径(默认值) configuration: map-underscore-to-camel-case: true # 开启下划线 ↔ 驼峰自动映射(默认开启) cache-enabled: false # 关闭二级缓存 global-config: db-config: id-type: assign_id # 全局主键策略:雪花算法 update-strategy: not_null # 更新策略:只更新非空字段
二、核心功能:解锁 MP 高级用法
2.1 条件构造器:灵活组装 WHERE 条件
MP 提供 Wrapper 条件构造器,支持所有 SQL WHERE 条件语法,无需手写 SQL 即可实现复杂查询 / 更新。核心实现类:
QueryWrapper:用于查询场景,可指定返回字段;UpdateWrapper:用于更新场景,可直接设置 SQL 片段。
案例 1:复杂条件查询
需求:查询用户名含「o」、余额 ≥ 1000 的用户,仅返回 id、username、info、balance 字段。
java
运行
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import java.util.List; @SpringBootTest public class WrapperTest { @Autowired private UserMapper userMapper; @Test void testQueryWrapper() { QueryWrapper<User> wrapper = new QueryWrapper<User>() // 指定返回字段 .select("id", "username", "info", "balance") // 模糊查询:username LIKE '%o%' .like("username", "o") // 大于等于:balance >= 1000 .ge("balance", 1000); List<User> users = userMapper.selectList(wrapper); users.forEach(System.out::println); } }
案例 2:条件更新(QueryWrapper)
需求:将用户名「jack」的用户余额改为 2000。
java
运行
@Test void testUpdateWithQueryWrapper() { // 1. 封装更新数据 User user = new User(); user.setBalance(2000); // 2. 封装更新条件 QueryWrapper<User> wrapper = new QueryWrapper<User>() .eq("username", "jack"); // 3. 执行更新 userMapper.update(user, wrapper); }
案例 3:更新 SQL 片段(UpdateWrapper)
需求:将 id 为 1、2、4 的用户余额扣减 200(非直接赋值,需写 SQL 片段)。
java
运行
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import java.util.Arrays; @Test void testUpdateWrapper() { UpdateWrapper<User> wrapper = new UpdateWrapper<User>() // 设置 SQL 片段:balance = balance - 200 .setSql("balance = balance - 200") // 条件:id IN (1,2,4) .in("id", Arrays.asList(1L, 2L, 4L)); // 第一个参数传 null,更新逻辑由 wrapper 实现 userMapper.update(null, wrapper); }
2.2 自定义 SQL:结合 Wrapper 实现复杂逻辑
MP 支持「Wrapper 构建条件 + 自定义 SQL」,兼顾灵活性与简洁性。
需求:将 id 在指定范围的用户余额扣减指定值。
步骤 1:Mapper 接口定义方法
java
运行
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; public interface UserMapper extends BaseMapper<User> { // 注意:@Param("ew") 是固定名称,不可修改 @Update("UPDATE tb_user SET balance = balance - #{amount} ${ew.customSqlSegment}") void updateBalanceByIds( @Param("ew") LambdaQueryWrapper<User> wrapper, @Param("amount") int amount ); }
步骤 2:调用自定义方法
java
运行
@Test void testCustomSqlWithWrapper() { // 1. 封装条件:id IN (1,2,4) LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>() .in(User::getId, Arrays.asList(1L, 2L, 4L)); // 2. 执行自定义 SQL userMapper.updateBalanceByIds(wrapper, 200); }
2.3 Service 层封装:更高效的 CRUD 工具
MP 为 Service 层提供 IService 和 ServiceImpl 封装,内置批量操作、分页查询等高频方法,比 BaseMapper 更易用。
步骤 1:定义 Service 接口与实现类
java
运行
// 1. Service 接口:继承 IService public interface UserService extends IService<User> { } // 2. Service 实现类:继承 ServiceImpl,指定 Mapper 和实体类 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { }
案例 1:Lambda 条件查询
需求:根据用户名关键字、状态、余额范围查询用户(参数可为空)。
java
运行
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import java.util.List; @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { public List<User> queryUserList(String name, Integer status, Integer minBalance, Integer maxBalance) { return lambdaQuery() // 条件:name 不为空时,模糊查询用户名 .like(name != null, User::getUsername, name) // 条件:status 不为空时,精准匹配状态 .eq(status != null, User::getStatus, status) // 条件:min/maxBalance 都不为空时,匹配余额范围 .between(minBalance != null && maxBalance != null, User::getBalance, minBalance, maxBalance) .list(); } }
案例 2:Lambda 条件更新
需求:扣减用户余额,若扣减后余额为 0 则冻结用户(status=2)。
java
运行
public void updateUserBalance(Long id, String username, int amount) { lambdaUpdate() // 扣减余额 .setSql("balance = balance - " + amount) // 余额为 0 时冻结 .set(amount == 0, User::getStatus, 2) // 条件:id 和 username 不为空时匹配 .eq(id != null, User::getId, id) .eq(username != null, User::getUsername, username) .update(); }
案例 3:批量新增(高性能)
需求:批量插入 10 万条用户数据(优化插入性能)。
- 首先在 JDBC 连接 URL 中添加参数(开启批处理优化):
yaml
spring: datasource: url: jdbc:mysql://localhost:3306/mp_db?rewriteBatchedStatements=true username: root password: 123456
- 编写批量插入代码:
java
运行
public void batchSave() { // 1. 初始化 10 万条测试数据 List<User> userList = new ArrayList<>(100000); for (int i = 0; i < 100000; i++) { User user = new User(); user.setUsername("test_" + i); user.setBalance(100); userList.add(user); } // 2. MP 批量插入(默认每 1000 条批量提交) saveBatch(userList); }
三、拓展功能:解决实际开发痛点
3.1 代码生成:一键生成 CRUD 代码
MP 支持通过插件 / 代码生成器一键生成 Entity、Mapper、Service、Controller 代码,彻底告别重复编码:
- 安装 IDEA 插件:
MyBatisX(官方推荐); - 配置数据库连接(Database 面板);
- 右键数据表 →
MyBatisX-Generator→ 配置生成规则 → 生成代码。
3.2 静态工具 Db:解决 Service 循环依赖
MP 提供静态工具类 Db,封装了 BaseMapper 核心方法,可直接调用,避免 Service 层相互注入导致的循环依赖:
java
运行
import com.baomidou.mybatisplus.extension.toolkit.Db; import java.util.List; // 直接查询 id 为 1 的用户 User user = Db.getById(1L, User.class); // 批量查询 List<User> users = Db.listByIds(Arrays.asList(1L,2L), User.class);
3.3 逻辑删除:避免物理删除数据
逻辑删除通过「标记字段」模拟删除效果(不真正删除数据),MP 可自动拦截 CRUD 操作:
步骤 1:配置逻辑删除(application.yml)
yaml
mybatis-plus: global-config: db-config: logic-delete-field: deleted # 逻辑删除字段名 logic-delete-value: 1 # 已删除标记 logic-not-delete-value: 0 # 未删除标记
步骤 2:实体类添加字段
java
运行
@TableField("deleted") private Integer deleted; // 0:未删除,1:已删除
配置完成后:
- 调用
deleteById(1L)时,MP 自动执行UPDATE tb_user SET deleted=1 WHERE id=1; - 调用
selectList()时,MP 自动添加条件WHERE deleted=0。
⚠️ 注意:逻辑删除会产生大量「垃圾数据」,需定期清理或迁移至历史表。
3.4 枚举处理器:实现枚举与数据库字段映射
数据库中状态字段(如 status)通常用 int 存储,实体类中用枚举更易读,MP 可通过注解实现自动映射:
步骤 1:枚举类添加注解
java
运行
import com.baomidou.mybatisplus.annotation.EnumValue; import lombok.Getter; @Getter public enum UserStatus { NORMAL(1, "正常"), FREEZE(2, "冻结"); // 标记与数据库对应的数值 @EnumValue private final int value; private final String desc; UserStatus(int value, String desc) { this.value = value; this.desc = desc; } }
步骤 2:配置全局枚举处理器
yaml
mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
步骤 3:实体类使用枚举
java
运行
private UserStatus status; // 自动映射:1 → NORMAL,2 → FREEZE
3.5 JSON 处理器:支持 JSON 类型字段
数据库中 JSON 类型字段(如 info),MP 可通过注解自动序列化 / 反序列化为实体类属性:
java
运行
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler; import com.baomidou.mybatisplus.annotation.TableField; // 数据库 info 字段为 JSON 类型,自动映射为 UserInfo 对象 @TableField(typeHandler = JacksonTypeHandler.class) private UserInfo info; // 嵌套 JSON 实体 @Data public class UserInfo { private Integer age; private String gender; private String intro; }
四、插件功能:分页插件(高频使用)
MP 提供内置拦截器插件,其中「分页插件」是开发中最常用的功能,支持物理分页。
4.1 配置分页插件
java
运行
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加分页插件(指定数据库类型) PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(); pageInterceptor.setDbType(com.baomidou.mybatisplus.annotation.DbType.MYSQL); pageInterceptor.setMaxLimit(1000L); // 限制最大分页条数(防止性能问题) interceptor.addInnerInterceptor(pageInterceptor); return interceptor; } }
4.2 分页查询实战
java
运行
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import org.junit.jupiter.api.Test; @Test void testPageQuery() { // 1. 分页参数:第 1 页,每页 5 条 Page<User> page = Page.of(1, 5); // 2. 排序参数:按 balance 降序 page.addOrder(new OrderItem("balance", false)); // 3. 执行分页查询(Service 层) Page<User> resultPage = userService.page(page); // 4. 获取分页结果 System.out.println("总条数:" + resultPage.getTotal()); System.out.println("总页数:" + resultPage.getPages()); System.out.println("当前页数据:" + resultPage.getRecords()); }
4.3 通用分页接口:标准化分页返回
实际开发中,分页接口需统一入参和出参格式,以下是通用实现方案:
步骤 1:定义分页入参(PageQuery)
java
运行
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.Data; @Data public class PageQuery { private Integer pageNo = 1; // 默认第 1 页 private Integer pageSize = 10; // 默认每页 10 条 private String sortBy; // 排序字段 private Boolean isAsc = true; // 是否升序 // 转换为 MP 分页对象(默认按 update_time 降序) public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() { return toMpPage("update_time", false); } // 转换为 MP 分页对象(自定义默认排序) public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc) { Page<T> page = Page.of(pageNo, pageSize); // 前端传了排序字段则用前端的,否则用默认 if (sortBy != null) { page.addOrder(new OrderItem(sortBy, isAsc)); } else { page.addOrder(new OrderItem(defaultSortBy, isAsc)); } return page; } }
步骤 2:定义分页出参(PageDTO)
java
运行
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import cn.hutool.core.bean.BeanUtil; import lombok.Data; import java.util.Collections; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; @Data public class PageDTO<V> { private Long total; // 总条数 private Long pages; // 总页数 private List<V> list; // 当前页数据 // 空结果封装 public static <V, P> PageDTO<V> empty(Page<P> page) { return new PageDTO<>(page.getTotal(), page.getPages(), Collections.emptyList()); } // PO 转 VO(自动拷贝) public static <V, P> PageDTO<V> of(Page<P> page, Class<V> voClass) { List<P> records = page.getRecords(); if (records.isEmpty()) { return empty(page); } List<V> voList = BeanUtil.copyToList(records, voClass); return new PageDTO<>(page.getTotal(), page.getPages(), voList); } // 自定义 PO 转 VO 逻辑 public static <V, P> PageDTO<V> of(Page<P> page, Function<P, V> convertor) { List<P> records = page.getRecords(); if (records.isEmpty()) { return empty(page); } List<V> voList = records.stream().map(convertor).collect(Collectors.toList()); return new PageDTO<>(page.getTotal(), page.getPages(), voList); } }
步骤 3:编写分页接口
java
运行
// 1. Controller 层 import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @GetMapping("/page") public PageDTO<UserVO> queryUserByPage(PageQuery query) { return userService.queryUserByPage(query); } } // 2. Service 层实现 @Service public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService { @Override public PageDTO<UserVO> queryUserByPage(PageQuery query) { // 1. 构建分页条件(默认按 update_time 降序) Page<User> page = query.toMpPageDefaultSortByUpdateTimeDesc(); // 2. 执行分页查询(可叠加 Lambda 条件) page(page, lambdaQuery().eq(User::getStatus, 1)); // 3. 转换为 VO 并返回 return PageDTO.of(page, UserVO.class); } }
接口测试
访问地址:http://localhost:8080/users/page?pageNo=1&pageSize=5&sortBy=balance&isAsc=false
返回结果示例:
json
{ "total": 1005, "pages": 201, "list": [ { "id": 1, "username": "Jack", "info": "{\"age\":21,\"gender\":\"male\",\"intro\":\"佛系青年\"}", "status": 1, "balance": 2000 } ] }
五、总结
MyBatis-Plus 作为 MyBatis 的增强工具,核心优势是「简化单表 CRUD、保留自定义 SQL 灵活性」:
- 基础层:
BaseMapper+IService实现零 XML 单表操作; - 进阶层:
Wrapper条件构造器实现复杂 WHERE 条件; - 拓展层:逻辑删除、枚举 / JSON 处理器解决实际开发痛点;
- 插件层:分页插件是高频必备功能,需熟练掌握。
掌握 MP 可大幅减少重复编码,提升开发效率,是后端开发者必备技能!