前言

今天系统性地复习并深入学习了 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. 怎么用?

  1. 配置 OptimisticLockerInnerInterceptor 插件。
  2. 数据库表添加 version 字段。
  3. 实体类字段加 @Version 注解。
  4. 关键点:更新前必须先查询(拿到旧 version),再更新。

2. 底层 SQL 原理(面试考点)

很多人以为 MP 是让数据库去执行 version = version + 1错!

MP 的拦截器工作流程如下:

  1. Java 层:取出你传入对象的 oldVersion (比如 1)。

  2. Java 层:计算 newVersion = 1 + 1 = 2

  3. 生成 SQL

    SQL

    UPDATE user SET ..., version = 2 WHERE id = ? AND version = 1
    

    它利用 WHERE version = oldVersion 来保证如果不一致就更新失败。


六、 总结

MyBatis-Plus 不仅仅是一个“偷懒工具”,它的一整套插件体系(分页、乐观锁、防全表更新)构成了企业级开发的安全基石。

  • 日常 CRUD:熟练使用 lambdaQuery 链式调用。
  • 复杂场景:果断回退到 XML 手写 SQL,不要死磕 Wrapper。
  • 高并发:Redis 分布式锁挡流量 + MP 乐观锁兜底数据一致性。

今日心得:工具的上限取决于使用者的认知。掌握底层原理,才能在开发中游刃有余。

Logo

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

更多推荐