MyBatis-Plus (MP) 避坑与进阶指南:从 CRUD 到企业级实战
前言
今天系统性地复习并深入学习了 MyBatis-Plus(简称 MP)。以前只觉得它是一个简化的 CRUD 工具,经过今天的深度剖析,才发现原来在企业级开发中,MP 有这么多“骚操作”和需要注意的底层细节。
本文总结了从基础注解、Lambda 优雅查询、Service 层与 Mapper 层的使用规范,到乐观锁底层原理等核心知识点。
一、 基础映射与注解:别让表名坑了你
1. @TableName 的必要性
在实体类中,我们经常看到 @TableName("tb_follow")。
- 作用:显式告诉 MP,这个实体类对应数据库里的哪张表。
- 为什么必须写? 默认情况下,MP 会把类名(如
Follow)转换为下划线命名(follow)去找表。如果你的数据库表名加了前缀(如tb_follow),不加注解就会报Table 'xxx.follow' doesn't exist。 - 偷懒技巧:如果全库表都有统一前缀,可以在
yml中配置全局前缀table-prefix,这样就可以省略注解。
二、 告别硬编码:LambdaQueryWrapper 的优雅之道
1. 为什么要用 Lambda?
以前写条件构造器,可能是这样的:
Java
queryWrapper.eq("user_name", "Jack"); // 危险!"user_name" 是魔法字符串
这种写法一旦数据库字段改名,编译期不报错,运行期直接炸。 企业级标准写法必须是 LambdaQueryWrapper:
Java
lqw.eq(User::getUserName, "Jack"); // 安全!利用方法引用
原理揭秘:为什么 User::getUserName 能变成数据库字段?MP 底层利用了 Java 的 SerializedLambda 机制,反向解析出方法名,再根据驼峰转下划线规则推导出数据库列名。
2. 动态 SQL 的终极杀招
在处理“前端传参可能为 null”的场景时,不要在代码里写一堆 if (name != null)。MP 的所有条件方法都支持一个 boolean condition 参数:
Java
// 优雅写法:只有当 name 不为空且不为空串时,才拼接 SQL
lqw.like(StringUtils.isNotBlank(name), User::getName, name);
// 只有当 age 不为 null 时,才拼接 > age
lqw.gt(age != null, User::getAge, age);
三、 代码写在哪?Service 层 vs Mapper 层
这是开发中最高频的纠结:查询逻辑到底写在 ServiceImpl 里,还是写在 Mapper 里?
1. 黄金法则
-
简单逻辑(Service 层一把梭): 单表查询、简单的条件过滤、校验逻辑。推荐使用
lambdaQuery()链式写法。Java
// ServiceImpl 中 User user = lambdaQuery() .eq(User::getPhone, phone) .one(); -
复杂逻辑(Mapper 层 + XML): 涉及多表 JOIN、复杂的报表统计、SQL 长度超过 10 行,或者该查询逻辑被多个 Service 复用。此时应在 DAO 层定义接口,在 XML 中手写 SQL。
2. 一个特例:Wrapper + XML 混合打法
如果 SQL 很复杂(如 JOIN),但 WHERE 条件又是动态的,可以这么写:
- Mapper:
selectJoin(@Param(Constants.WRAPPER) Wrapper wrapper) - XML:
SELECT ... FROM t1 JOIN t2 ... ${ew.customSqlSegment}这样既保留了 Service 层动态拼条件的便利,又解决了复杂 SQL 的结构问题。
四、 那些容易踩的“坑”
1. 分页拦截器失效
写了 Page 对象,结果查出来还是全部数据? 原因:MP 的物理分页依赖插件重写 SQL。必须在配置类中添加 MybatisPlusInterceptor 并加入 PaginationInnerInterceptor。
2. selectBatchIds 的返回值
Java
// 传入 ID:[1, 2, 99] (99 不存在)
List<User> users = userDao.selectBatchIds(ids);
注意:返回的 List 长度是 2,而不是 3!不存在的 ID 会被忽略,不会返回 null 占位。
3. 空列表炸弹
调用 deleteBatchIds(list) 或 selectBatchIds(list) 时,务必判空!如果传入空集合,可能会生成错误的 SQL(如 IN ())导致程序崩溃。
五、 并发安全:深入理解乐观锁
在涉及资金、库存等核心业务时,MP 的乐观锁插件是最后一道防线。
1. 怎么用?
- 配置
OptimisticLockerInnerInterceptor插件。 - 数据库表添加
version字段。 - 实体类字段加
@Version注解。 - 关键点:更新前必须先查询(拿到旧 version),再更新。
2. 底层 SQL 原理(面试考点)
很多人以为 MP 是让数据库去执行 version = version + 1。错!
MP 的拦截器工作流程如下:
-
Java 层:取出你传入对象的 oldVersion (比如 1)。
-
Java 层:计算
newVersion = 1 + 1 = 2。 -
生成 SQL:
SQL
UPDATE user SET ..., version = 2 WHERE id = ? AND version = 1它利用
WHERE version = oldVersion来保证如果不一致就更新失败。
六、 总结
MyBatis-Plus 不仅仅是一个“偷懒工具”,它的一整套插件体系(分页、乐观锁、防全表更新)构成了企业级开发的安全基石。
- 日常 CRUD:熟练使用
lambdaQuery链式调用。 - 复杂场景:果断回退到 XML 手写 SQL,不要死磕 Wrapper。
- 高并发:Redis 分布式锁挡流量 + MP 乐观锁兜底数据一致性。
今日心得:工具的上限取决于使用者的认知。掌握底层原理,才能在开发中游刃有余。
更多推荐


所有评论(0)