本文同步更新于微信公众号:
mp.weixin.qq.com/s/j8__DcSTy...
theme: awesome-green
前言
日常Java后台开发中,绝大多数业务都是单表增删改查。原生MyBatis虽然灵活,但存在大量重复劳动:每张表都要手写insert、update、select、delete的XML语句,动态条件拼接需要手动处理字符串判空,分页、时间戳、软删除等通用能力需要自己封装工具类,开发效率低且重复代码难以维护。
本文带你从零入门MyBatis-Plus,理清核心能力与基础用法,适合新手快速上手。
一、背景:MyBatis-Plus 解决了什么问题
1. MyBatis 的痛点
-
单表CRUD必须手动编写XML/注解SQL,实体越多,重复代码越多;
-
动态查询条件需要手动拼接字符串,极易出现SQL注入、括号逻辑错乱;
-
分页、逻辑删除、创建更新时间、并发乐观锁、分布式主键等通用功能无内置实现,每个项目重复封装;
-
实体与数据库字段映射、多条件查询样板代码冗余。
2. MyBatis-Plus(简称MP)定位
MP 是**「MyBatis 的增强工具,只增强、不修改」**,完全兼容原生MyBatis所有能力,底层依旧依赖MyBatis执行SQL。
核心目标:「消除单表CRUD冗余代码,内置企业级通用持久层能力,大幅提升后台开发效率」。
3. 核心优势
-
无侵入:原有MyBatis XML、自定义SQL完全保留,可平滑迁移;
-
开箱即用:CRUD、分页、条件构造器无需手写SQL;
-
内置通用语法糖:逻辑删除、自动填充、乐观锁、分布式主键;
-
Lambda语法构造条件,编译校验字段,杜绝字符串硬编码错误。
适用场景:管理后台、CRUD为主业务、中小型服务;复杂多表联查、统计报表依旧可以手写原生MyBatis XML。
二、MyBatis-Plus 核心增强功能
2.1 强大CRUD封装:BaseMapper + IService 双层持久层
MP 提供两套分层CRUD封装,分别对应DAO层与Service层,职责清晰。
1)DAO层:BaseMapper(Mapper接口继承)
所有Mapper接口继承BaseMapper<T>,无需编写任何方法,自动拥有单表基础操作能力,直接操作数据库。
java
// Mapper接口,无需自定义CRUD方法
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
常用内置方法:
-
insert(T entity):新增单条数据; -
selectById(id)、deleteById(id)、updateById(entity):根据主键查询/删除/更新; -
selectList(Wrapper)、delete(Wrapper):按条件批量查询、批量删除; -
selectPage(Page, Wrapper):分页查询。 -
「自定义SQL方法」:开发者可在Mapper接口中自定义任意抽象方法,配合XML/注解编写专属SQL,和BaseMapper自带方法共存使用。
2)Service层:IService + ServiceImpl(业务层封装)
业务层做二次封装,在BaseMapper基础上新增批量操作、链式查询、语义化方法,规范业务代码分层。
-
接口:继承
IService<T>; -
实现类:继承
ServiceImpl<M,T>(内置注入Mapper,封装通用逻辑)。
java
// Service接口
public interface UserService extends IService<User> {
}
// Service实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
IService独有增强能力:
-
批量操作:
saveBatch批量新增、updateBatchById批量更新; -
链式Lambda查询:
lambdaQuery().eq().list()极简写法; -
语义化方法:
getById、list、page,区分DAO层底层方法,业务可读性更强。 -
「主键批量操作(高频实用)」:IService 内置开箱即用批量主键操作方法,无需手动拼接 IN 条件,底层优化批量SQL性能:
listByIds(Collection<ID> idList):根据主键批量查询多条数据removeByIds(Collection<ID> idList):根据主键批量删除数据updateBatchById(Collection<T> entityList):批量更新多条数据saveBatch(Collection<T> entityList):批量新增数据
ini
// 批量查询
List<Long> idList = Arrays.asList(1L,2L,3L);
List<User> userList = userService.listByIds(idList);
// 批量删除
userService.removeByIds(idList);
两者区别总结
| 类 | 层级 | 核心特点 | 使用场景 |
|---|---|---|---|
| BaseMapper | Mapper/DAO | 底层原生数据库操作,无批量封装 | 简单单条操作、自定义Mapper内部调用 |
| IService | Service业务层 | 封装批量、链式查询、事务友好 | 项目标准分层开发,Controller直接调用 |
2.2 条件构造器 Wrapper:动态拼接Where条件(重点完整版)
Wrapper 是 MyBatis-Plus 最核心的语法糖,「彻底解决原生MyBatis动态SQL繁琐拼接问题」。无需写XML、无需拼接字符串、无需判空,链式调用自动生成安全的 WHERE 条件,同时杜绝 SQL 注入风险。
1)四大构造器介绍(项目固定选型)
Wrapper 分为查询、更新两大体系,「开发一律优先 Lambda 版本」,编译校验不报错、重构不翻车。
- 「QueryWrapper」:查询/删除,字段写字符串(硬编码,不推荐)
- 「LambdaQueryWrapper」:查询/删除,Lambda方法引用(企业首选)
- 「UpdateWrapper」:更新条件,字符串字段(不推荐)
- 「LambdaUpdateWrapper」:更新条件,Lambda安全写法(企业首选)
ini
// 快速创建方式(推荐)
LambdaQueryWrapper<User> lqw = Wrappers.lambdaQuery();
LambdaUpdateWrapper<User> luw = Wrappers.lambdaUpdate();
2)常用条件语法全覆盖
下面是开发 「99% 场景」 会用到的条件,整理成可直接复制的模板。
① 等值条件(eq / ne)
-
eq:等于 =
-
ne:不等于 <>
perl
// name = "张三" AND age != 18
lqw.eq(User::getName, "张三")
.ne(User::getAge, 18);
② 区间大小条件(gt / ge / lt / le / between)
-
gt:大于 >
-
ge:大于等于 >=
-
lt:小于 <
-
le:小于等于 <=
-
between:区间范围
less
// age > 18 AND age <= 60
lqw.gt(User::getAge, 18)
.le(User::getAge, 60);
// age BETWEEN 18 AND 30
lqw.between(User::getAge, 18, 30);
③ 模糊查询(like 系列)
-
like:前后模糊 %值%
-
likeRight:右模糊 值%
-
likeLeft:左模糊 %值
less
// 姓名包含"李"
lqw.like(User::getName, "李");
// 手机号以 138 开头
lqw.likeRight(User::getPhone, "138");
④ 空值判断(isNull / isNotNull)
less
// 邮箱为 NULL
lqw.isNull(User::getEmail);
// 手机号不为空
lqw.isNotNull(User::getPhone);
⑤ 包含集合(in / notIn)
less
// 部门ID在 1、2、3 中
lqw.in(User::getDeptId, Arrays.asList(1L,2L,3L));
3)逻辑关系:AND / OR 与括号嵌套(重点难点)
「默认规则」 :链式连续调用默认是AND 关系。
「普通OR」:直接拼接 or()
perl
// name=张三 OR age>18
lqw.eq(User::getName, "张三")
.or()
.gt(User::getAge, 18);
「带括号复杂逻辑(必须用lambda子表达式)」
场景:name = 张三 AND (age > 18 OR deptId = 2)
perl
lqw.eq(User::getName, "张三")
.and(w -> w.gt(User::getAge, 18).or().eq(User::getDeptId, 2));
场景:name = 张三 OR (age > 18 AND deptId = 2)
perl
lqw.eq(User::getName, "张三")
.or(w -> w.gt(User::getAge, 18).eq(User::getDeptId, 2));
💡 核心要点:「只要需要括号,必须使用 and(λ) / or(λ) 嵌套」,否则SQL优先级错乱。
4)condition 动态条件(开发最常用、消除大量if)
Wrapper 所有条件方法**「第一个参数为 boolean condition」**:
-
「condition = true」:拼接该SQL条件
-
「condition = false」:直接忽略该条件,不拼接
专门用于:前端传参、非必填查询条件,「彻底消灭 if 判断嵌套」。
ini
// 前端参数
String name = "";
Integer minAge = 18;
// 有姓名才模糊查,有最小年龄才判断
lqw.like(StringUtils.hasText(name), User::getName, name)
.ge(minAge != null, User::getAge, minAge);
等价传统写法,但代码精简 80%:
scss
if (StringUtils.hasText(name)) {
lqw.like(User::getName, name);
}
if (minAge != null) {
lqw.ge(User::getAge, minAge);
}
5、字段查询过滤(指定返回字段)
Wrapper 支持 select() 方法自定义查询字段,只返回需要的列,避免查询全字段、查询大文本字段,优化SQL性能、减少返回数据体积,是日常优化高频用法。只支持 Lambda 安全写法,杜绝字段硬编码。
ini
// 只查询 id、姓名、手机号,忽略其他所有字段
LambdaQueryWrapper<User> lqw = Wrappers.lambdaQuery();
lqw.select(User::getId, User::getName, User::getPhone)
.eq(User::getDeleted, 0);
List<User> userList = userMapper.selectList(lqw);
适用场景:列表页展示、分页查询、接口返回精简数据,过滤冗余大字段(头像、备注、大文本)。
6)更新构造器与核心注意事项
更新构造器(LambdaUpdateWrapper)用于构建动态更新条件,实现安全的局部字段更新,是业务开发中替代实体全量更新的主流写法。
1、基础使用示例
通过链式set指定需要更新的字段,搭配where条件精准更新数据,仅修改声明的字段,其余数据库字段保留原值。
ini
// 将部门ID=1的所有用户,年龄统一更新为25
LambdaUpdateWrapper<User> luw = Wrappers.lambdaUpdate();
// 仅设置需要更新的字段
luw.set(User::getAge, 25)
.eq(User::getDeptId, 1L);
// 第一个参数传null:表示不使用实体对象赋值,全部由Wrapper控制更新逻辑
userMapper.update(null, luw);
2、开发核心注意事项(两大高频坑点)
「注意事项1:普通set() 存在空值覆盖数据风险」
原生无参set()无自动判空逻辑,若传入null、空字符串,会强制覆盖数据库原有字段值。前端局部编辑、动态传参场景极易造成数据清空。解决方案:使用**「带条件set(condition)**「自定义非空判断,或直接使用」**setIfPresent」**自动忽略null值,规避空值覆盖问题。
「注意事项2:updateById 实体更新存在全量覆盖坑」
updateById属于**「全量字段覆盖更新」,原理是根据实体主键匹配数据,实体中所有null字段会直接清空数据库对应原值。仅适合完整实体保存、全字段修改场景,「绝对不适合局部编辑」**。
「核心选型总结」:日常局部动态更新,优先使用 Wrapper 链式更新(增量更新、安全不丢数据);避免直接使用 updateById 处理部分字段修改。
2.3 分页功能:开箱即用物理分页
MP提供内置分页拦截器,无需手写limit,自动适配MySQL、Oracle等多数据库,纯物理分页,性能稳定。
1. 前置配置(必须注册分页插件)
java
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// MySQL分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
2. Mapper层分页使用
ini
// 参数1:当前页,参数2:每页条数
Page<User> page = new Page<>(1, 10);
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery().eq(User::getDeleted,0);
Page<User> pageResult = userMapper.selectPage(page, wrapper);
// 分页结果获取
List<User> dataList = pageResult.getRecords(); // 当前页数据
long total = pageResult.getTotal(); // 总条数
long pages = pageResult.getPages(); // 总页数
3. Service层极简分页(推荐业务使用)
perl
// 链式lambda分页
Page<User> pageData = userService.lambdaQuery()
.eq(User::getAge, 18)
.page(new Page<>(1,10));
4. 自定义SQL分页(多表联查/复杂统计 必备)
上述分页方式仅适用于 MP 链式单表查询,实际开发中,「多表联查、子查询、复杂统计、自定义条件SQL」 无法使用 Wrapper,需要手写 XML 自定义SQL。
MP 分页插件完美支持**「自定义SQL分页」**,无需手动写 count 总数、无需拼接 limit,只需遵循固定格式,插件自动 「拦截SQL、自动分页、自动统计总条数」。
核心规则:「Mapper方法第一个参数必须是 Page 对象,返回值必须是 Page 对象」。
第一步:Mapper 接口定义方法
less
// 自定义多表分页查询
Page<UserVO> selectUserPage(@Param("page") Page<UserVO> page,
@Param("name") String name);
第二步:XML 手写自定义SQL(无需写limit、无需写count)
sql
<select id="selectUserPage" resultType="com.xxx.vo.UserVO">
SELECT u.id, u.name, u.phone, d.dept_name
FROM sys_user u
LEFT JOIN sys_dept d ON u.dept_id = d.id
<where>
<if test="name != null and name != ''">
AND u.name LIKE CONCAT('%',#{name},'%')
</if>
AND u.deleted = 0
</where>
</select>
第三步:业务层调用
ini
// 构建分页参数
Page<UserVO> page = new Page<>(1,10);
// 调用自定义分页方法
Page<UserVO> resultPage = userMapper.selectUserPage(page, "张三");
// 获取分页数据
List<UserVO> records = resultPage.getRecords();
long total = resultPage.getTotal();
关键说明
- 「无需手动分页」:MP插件自动拦截SQL,拼接 LIMIT 分页语句
- 「无需手动COUNT」:插件自动生成 count 查询统计总条数
- 「支持返回VO」:自定义分页可以返回自定义视图对象,不限于实体类
- 「适配所有复杂SQL」:多表联查、分组统计、子查询全部支持
5. 分页扩展特性(性能&容错优化)
-
「关闭Count统计(大数据分页性能优化)」 :对于海量数据列表、无需展示总条数/总页数的导出、滚动加载场景,可手动关闭count总数查询,避免全表统计耗时,大幅提升分页性能。构造Page对象时传入第三个参数 false 即可:
new Page<>(1,10,false)。 -
「分页溢出容错」 :开启插件溢出适配,当请求页码超过最大页数时,自动跳转到最后一页,避免返回空数据,配置:
setOverflow(true)。 -
「兼容自定义复杂SQL分页」:手写XML多表联查、子查询、分组统计等复杂SQL,可直接传入Page对象,MP分页插件自动拦截SQL、自动处理分页Limit与总数统计,无需手动编写分页逻辑。
2.4 其他实用语法糖(企业开发标配 + 完整配置)
1)逻辑删除(软删除)
不物理删除数据,通过deleted字段标记删除状态,适配数据溯源、恢复场景。
- yml全局配置
arduino
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-not-delete-value: 0 # 未删除
logic-delete-value: 1 # 已删除
- 实体增加标记字段
java
@TableLogic
private Integer deleted;
生效原理:无需业务代码干预,调用 Mapper 层 delete 删除方法时,底层自动将 DELETE 语句转为 UPDATE 更新语句;所有查询、分页、列表查询会自动拼接 WHERE deleted = 0,自动过滤已删除数据,无需手动处理。
2)自动填充 创建/更新时间
无需业务代码手动set时间,新增、修改时自动赋值create_time、update_time。
- 实体字段注解
ini
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
- 注册全局自动填充处理器(核心配置,必须配置)
新建自定义填充类,实现 MetaObjectHandler,交给Spring管理,统一自动填充创建时间、更新时间、创建人、更新人等字段。
kotlin
@Component
public class MyMetaHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
// 新增时自动填充创建时间、更新时间
this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
}
@Override
public void updateFill(MetaObject metaObject) {
// 修改时只更新更新时间
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
}
}
生效原理:新增/修改数据时,MP 自动触发填充逻辑,无需业务代码手动 set 时间字段,全局统一规范。
3)乐观锁(并发更新防覆盖)
高并发场景防止多人同时更新覆盖数据,依靠version版本号实现无锁并发控制。
- 实体添加版本字段
java
@Version
private Integer version;
- 注册乐观锁插件(核心配置,必须配置)
在 MP 全局配置类中添加乐观锁拦截器,否则版本号不会自动递增、并发控制失效。
java
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mpInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 乐观锁插件(新增)
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
-
更新执行逻辑:更新数据时自动携带 where 版本条件,更新成功后版本号自动+1;若并发导致版本不匹配,更新行数为0,更新失败,避免数据覆盖。
-
更新逻辑:更新时携带原版本号,版本匹配才更新,版本不匹配更新失败。
4)主键策略 @TableId
统一管理主键生成规则,适配单机自增、分布式唯一ID:
kotlin
@TableId(type = IdType.AUTO)
private Long id;
可通过 YAML 全局统一配置,无需每个实体单独注解(可选):
python
mybatis-plus:
global-config:
db-config:
id-type: assign_id # 全局默认雪花算法
常用策略说明:
-
AUTO:数据库自增(MySQL单机); -
ASSIGN_ID:雪花算法,分布式Long唯一ID(MP默认); -
ASSIGN_UUID:字符串UUID主键。
MyBatis-Plus 除基础CRUD与语法糖外,内置大量企业级高阶插件与防护能力,适配生产环境安全、性能、SaaS多租户、数据脱敏等场景,全部开箱即用。
2.5 高阶增强功能(企业级进阶能力)
1)SQL 日志打印(开发调试必备)
全局开启SQL日志,完整打印执行SQL、参数、执行耗时,方便排查BUG、优化SQL。
yml配置:
yaml
mybatis-plus:
configuration:
# 开启SQL日志输出
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
效果:控制台完整输出最终执行的SQL语句、查询参数、结果行数,可直接复制到数据库执行调试。
2)SQL 性能分析插件(慢SQL监控)
用于开发、测试环境监控慢SQL,拦截超时SQL,避免劣质SQL上线导致数据库卡顿。可自定义超时阈值,超时自动打印警告日志。
ini
@Bean
public MybatisPlusInterceptor mpInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 慢SQL监控:超时200ms告警
PerformanceInnerInterceptor performanceInterceptor = new PerformanceInnerInterceptor();
performanceInterceptor.setMaxTime(200);
interceptor.addInnerInterceptor(performanceInterceptor);
return interceptor;
}
生产环境建议关闭,仅用于开发调优。
3)防全表更新/删除(生产安全核心)
MP 内置**「防误操作安全机制」**,默认禁止无条件全表更新、全表删除,彻底杜绝开发人员误写空条件导致的数据清空事故,生产刚需。
原理:当 Wrapper 无任何查询条件、直接执行 update、delete 时,直接抛出异常阻断SQL执行。
如需临时关闭(超级管理员、后台批量操作场景),可手动配置放行,禁止业务代码随意关闭。
4)数据脱敏插件(隐私数据防护)
内置字段自动脱敏能力,无需手动代码处理,查询返回自动脱敏,适配手机号、身份证、邮箱、地址等隐私字段。
支持注解配置脱敏规则:掩码、中间隐藏、首尾展示等,新增/修改数据库存储原始明文,「仅查询返回脱敏」,不影响数据库真实数据。
5)多租户插件(SaaS系统必备)
MP 官方内置多租户数据隔离插件,适配 SaaS、多商户、多团队共用一套数据库场景,实现**「代码零侵入全局数据隔离」**。
核心原理:拦截所有CRUD SQL,自动追加租户条件,无需业务代码手动拼接租户ID。
核心细节:
- 默认租户字段:
tenant_id(可自定义任意字段名,如 merchant_id、company_id) - 租户ID由业务拦截器解析Token后存入 ThreadLocal,插件仅读取不生成
- 支持自定义忽略表(字典表、公共配置表、租户表本身不隔离)
- 支持超级管理员关闭租户隔离,查询全量数据
核心配置示例:
less
interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
// 获取当前登录租户ID
@Override
public Long getTenantId() {
return UserContext.getTenantId();
}
// 自定义租户字段名(可不写,默认tenant_id)
@Override
public String getTenantField() {
return "tenant_id";
}
// 忽略隔离的公共表
@Override
public boolean ignoreTable(String tableName) {
return Arrays.asList("sys_dict","sys_tenant").contains(tableName);
}
}));
6)游标分页(大数据量高性能分页)
区别于普通 offset 分页,「游标分页适配百万级大数据列表、数据导出场景」,解决传统分页越往后越慢的性能痛点。
优势:
- 无需 count 统计总数,性能极高
- 基于主键游标滚动查询,避免深分页偏移性能衰减
- 适合批量数据同步、文件导出、数据清洗场景
三、入门快速上手步骤(极简流程)
-
SpringBoot项目引入
mybatis-plus-boot-starter依赖,移除原生MyBatis启动器; -
application.yml配置数据库、开启下划线转驼峰、打印SQL日志;
-
实体类使用
@TableName、@TableId、@TableField映射数据库字段; -
Mapper接口继承
BaseMapper,启动类添加@MapperScan扫描Mapper; -
Service层实现
IService+ServiceImpl分层; -
需要分页、乐观锁则注册对应插件;
-
使用LambdaWrapper拼接动态条件,调用内置CRUD方法完成业务。
四、总结
-
MP核心价值:封装单表通用操作,把开发人员从重复SQL中解放;
-
分层CRUD:BaseMapper负责底层DAO,IService适配业务层批量、链式操作;
-
Wrapper条件构造器优先使用Lambda写法,配合condition动态条件,彻底消灭冗余if,支持复杂and/or括号逻辑;
-
分页、逻辑删除、自动填充、乐观锁、主键策略均为内置语法糖(2.4章节),开箱即用;高阶能力(2.5章节)支持SQL监控、数据脱敏、多租户隔离、大数据游标分页、生产防误删防护;
-
MP不替代MyBatis,复杂多表、统计查询依旧支持自定义XML,两者可共存。
参考文献
1 MyBatis-Plus 官方文档. baomidou.com/
2 MyBatis 官方使用文档. mybatis.org/mybatis-3/
❝
(注:部分内容可能由 AI 生成)
❞
本文使用 markdown.com.cn 排版