【MyBatis-Plus | 常见问题与面试重点】

MyBatis-Plus(简称 MP)极大地提升了开发效率,但如果不理解其原理和细节,很容易掉入"陷阱"。下面我将从 常见错误与坑点中大厂面试题 两个方面进行系统性的讲解。


第一部分:MyBatis-Plus 常见错误与坑点

这些是实战中高频出现的问题,理解它们能帮你节省大量调试时间。

1. 实体类映射与注解问题

坑点1:@TableField 注解误用

  • 场景 : 实体类中的字段名和数据库列名不一致,但没有正确使用 @TableField

    • 数据库列:user_name

    • 实体字段:username (未加注解)

  • 错误结果 : MP 默认使用驼峰转下划线,username 会被映射为 username,导致查询不到数据。

  • 解决方案

    • 如果开启了下划线转驼峰,则不需要注解。

    • 如果不满足默认规则,使用 @TableField("user_name") 显式指定。

    • 如果字段是数据库不存在的(如业务逻辑字段),使用 @TableField(exist = false)

坑点2:主键策略 @TableId 混淆

  • 场景: 特别是使用分布式 ID(如 Snowflake)时。

  • 错误 : 数据库主键是自增(AUTO_INCREMENT),但在实体类上配置了 @TableId(type = IdType.ASSIGN_ID)。插入时,MP 会生成一个 Long 类型的 ID 传入,而数据库期望自增,导致冲突。

  • 解决方案

    • 数据库自增:使用 @TableId(type = IdType.AUTO)

    • 使用 MP 的分布式 ID:使用 @TableId(type = IdType.ASSIGN_ID),并且数据库字段类型应为 BIGINT

    • 全局配置 : 在 application.yml 中配置 mybatis-plus.global-config.db-config.id-type=assign_id

坑点3: Lombok 与 MP 的"冤家"关系

  • 场景 : 使用 @Data 等 Lombok 注解,但 MP 在底层可能通过 getter/setter 进行反射操作。如果 Lombok 的生成规则与 MP 期望不符(虽然不常见),可能导致序列化、类型处理等问题。

  • 解决方案: 确保 Lombok 版本与 MP 兼容。在极端情况下,如果遇到诡异问题,可以尝试手写 getter/setter 来排除。

2. 查询与条件构造器问题

坑点4:条件构造器 eq() 传入 null

  • 场景QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("name", request.getName()); 如果 request.getName()null,生成的 SQL 会是 WHERE name = null,这不会返回任何结果(因为 SQL 中 = null 是无效的,应该用 IS NULL)。

  • 解决方案

    • 手动判空:if (name != null) wrapper.eq("name", name);

    • 使用条件构造器的条件方法:wrapper.eq(name != null, "name", name)。第一个参数为 true 时,该条件才被拼接。

坑点5:模糊查询未手动添加通配符 %

  • 场景wrapper.like("name", "张"),生成的 SQL 是 name LIKE '%张%',这是正确的。但如果你想要的是"张%"(前缀匹配),MP 没有直接的方法。

  • 错误wrapper.likeRight("name", "张") 期望生成 name LIKE '张%',但有时开发者会忘记这个 API,试图自己拼接。

  • 解决方案

    • like("name", "张") -> %张%

    • likeLeft("name", "张") -> %张

    • likeRight("name", "张") -> 张%

    • 绝对不要用字符串拼接的方式 wrapper.apply("name LIKE '" + name + "%'"),有 SQL 注入风险!

坑点6:QueryWrapper 误用导致全表更新/删除

  • 场景 : 在使用 update()delete() 时,如果没有给 UpdateWrapperQueryWrapper 设置条件,生成的 SQL 会是 UPDATE table SET ...DELETE FROM table,导致灾难性后果。

  • 解决方案

    • 代码审查: 这是最重要的防线。

    • 使用 MP 的插件 : 配置 BlockAttackInnerInterceptor 攻击阻断插件,可以防止全表更新和删除。

java

复制代码
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加攻击阻断插件
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
        return interceptor;
    }
}
3. 服务层与分页问题

坑点7:MP 分页插件配置缺失或错误

  • 场景 : 调用 page(...) 方法后,发现分页信息(total)不对,或者根本没有分页效果。

  • 错误原因: 没有配置分页插件,或者配置在了其他拦截器后面(在 MyBatis 中,拦截器的顺序很重要)。

  • 解决方案: 确保正确配置分页插件。

java

复制代码
@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件,必须添加到拦截器链中,并且通常放在最前面
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

坑点8:在 Service 的默认方法中"空转"

  • 场景boolean success = userService.save(user); 然后判断 success,但发现永远是 true

  • 错误原因 : MP 的 IService 的默认 save 方法返回的是 boolean,但其底层实现是 retBool 方法,它判断的是受影响的行数 > 0。然而,如果你的主键是 MP 生成的,即使插入失败,也可能因为其他原因(如后续的插件执行)返回 true不要完全依赖这个返回值做严格的业务逻辑判断。

  • 最佳实践: 更可靠的判断方式是插入后获取主键,或者通过捕获异常来处理。

4. 性能与高级特性问题

坑点9:逻辑删除与唯一索引的冲突

  • 场景 : 对某个字段(如 username)加了唯一索引,并开启了逻辑删除。当删除一个用户后,再创建一个同名用户时,会报唯一索引冲突。

  • 原因 : 逻辑删除只是把 deleted 字段从 0 改为 1,那条记录依然在数据库中。新插入的同名记录与已删除的记录在 username 上冲突。

  • 解决方案

    1. deleted 字段也加入唯一索引中(如 UNIQUE KEY uk_username (username, deleted)),但需要确保 deleted 的默认值(0)和删除后的值(1)能区分开。

    2. 使用不同的删除标识,例如,不使用 0/1,而是使用 0 和主键 ID(或其他唯一值),这样 (username, deleted) 的组合永远是唯一的。

坑点10:大字段查询导致的性能问题

  • 场景 : 实体类中有一个 String content 字段对应 TEXT 类型。在普通的 list() 查询时,会把这个大字段也查出来,占用大量网络和内存。

  • 解决方案

    • 使用 @TableField(select = false) 注解,默认不查询该字段。

    • 在需要的时候,使用 QueryWrapperselect() 方法显式指定需要查询的字段。


第二部分:中大厂面试题

中大型互联网公司不仅会问怎么用,更会问 "为什么这么用""如何优化"

1. 基础与原理
  1. MyBatis-Plus 和 MyBatis 有什么区别?它主要解决了哪些痛点?

    • 考察点: 对 MP 定位的理解。

    • 参考答案: MP 是 MyBatis 的增强工具,只做增强不做改变。核心解决了:1)通用 CRUD 的样板代码问题;2)强大的条件构造器;3)分页、性能分析等常用功能的插件化;4)代码生成器等。

  2. 请讲讲 MyBatis-Plus 的条件构造器(Wrapper)的工作原理?

    • 考察点: 对源码的初步了解。

    • 参考答案 : Wrapper 内部维护了一个 List<Segment> 集合(SQL 片段),如 eq, like 等方法都会向这个集合添加一个片段。在最终执行 SQL 时,通过 getCustomSqlSegment 方法将这些片段按照一定的逻辑(例如,处理 ANDOR,过滤 null 值等)拼接成完整的 WHERE 子句。

  3. MyBatis-Plus 是如何实现分页的?它的分页插件原理是什么?

    • 考察点: 插件机制和分页实现。

    • 参考答案 : 通过实现 MyBatis 的 Interceptor 接口。在 Executor 执行 SQL 前进行拦截。首先,查询总数(通过解析原始 SQL,拼接 COUNT(1) 的查询);然后,对原始 SQL 根据不同的数据库方言拼接 LIMIT 等分页关键字。最后执行两条 SQL,并将结果设置到 Page 对象中。

2. 设计与扩展
  1. 如果让你设计一个多租户(SaaS)系统,如何使用 MyBatis-Plus 实现数据隔离?

    • 考察点: 对 MP 插件机制和实际业务场景的结合能力。

    • 参考答案 : 使用 MP 的 TenantLineInnerInterceptor 租户插件。实现 TenantLineHandler 接口,在其中指定租户 ID 的字段名(如 tenant_id)和如何获取当前租户的 ID(通常从 ThreadLocal 或 Security Context 中获取)。该插件会自动在所有涉及查询、更新、删除的 SQL 上追加 tenant_id = ? 条件。

  2. 如何基于 MyBatis-Plus 实现一个通用的"数据权限"功能?

    • 考察点: 更复杂的插件和 SQL 改写能力。

    • 参考答案 : 这也是通过自定义拦截器实现。可以定义一个注解,标注在 Mapper 方法上,指明需要的权限规则(如 {dept_id} = #{user.deptId})。在拦截器中,解析该注解,并根据当前登录用户的信息,动态地将权限规则拼接至 SQL 的 WHERE 条件中。这比多租户更复杂,因为规则是动态多样的。

  3. MyBatis-Plus 的代码生成器原理是什么?如果让你定制一个,你会怎么做?

    • 考察点: 对代码生成和模板引擎的理解。

    • 参考答案 : MP 的代码生成器底层使用了 Apache Velocity 模板引擎。它通过 JDBC 读取数据库的元数据(表结构、字段、注释等),然后将这些数据填充到预设的 .vm 模板文件中,从而生成 Entity、Mapper、Service 等代码。如果要定制,可以修改这些模板文件,或者继承 AutoGenerator 并重写相关方法。

3. 陷阱与优化
  1. 在使用 MyBatis-Plus 的过程中,你遇到过哪些性能问题?是如何优化的?

    • 考察点: 实战经验和问题排查能力。

    • 参考答案

      • N+1 问题 : 虽然 MP 提供了 @One@Many 注解,但在复杂场景下容易导致 N+1 查询。优化方式是手动编写连表查询的 SQL,而不是依赖自动映射。

      • 大字段查询 : 如前所述,使用 select 控制查询字段。

      • 全表更新: 强调使用攻击阻断插件。

      • 逻辑删除与索引: 强调唯一索引的问题和解决方案。

  2. @CacheNamespace 注解和 MyBatis-Plus 的二级缓存机制你了解吗?在分布式环境下有什么坑?

    • 考察点: 对缓存和分布式系统的理解。

    • 参考答案 : MyBatis 原生支持二级缓存,默认是单机版的 PerpetualCache。在分布式环境下,如果多个应用实例同时操作数据库,会导致缓存数据不一致。解决方案是使用集中式缓存,如 Redis,并通过 MybatisRedisCache 等实现类来替换默认的缓存实现。

总结

类别 核心要点
常见坑点 注解映射、主键策略、条件构造器判空、模糊查询、全表操作、分页配置、逻辑删除冲突
面试重点 原理 (Wrapper、分页插件)、设计 (多租户、数据权限)、优化 (N+1、缓存)、源码(代码生成器)

要学好 MyBatis-Plus,不仅要会用其便捷的 API,更要理解其背后的 MyBatis 原理、插件机制,并时刻关注性能和安全问题。在面试中,结合具体业务场景来阐述你的理解和解决方案,会大大加分。

相关推荐
奔跑的蜗牛ing21 小时前
CentOS Nginx 安装与静态文件服务器配置指南
前端·面试·架构
KaifuZeng1 天前
通信与接口协议面试五、RS422
嵌入式硬件·面试·通信与接口
SamDeepThinking1 天前
组合还是继承?我在项目里主要看这两个判断
java·后端·面试
wear工程师1 天前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
SamDeepThinking1 天前
为什么很多人学了Java,还是没真正理解类和对象?
java·后端·面试
阿新聊ai1 天前
Claude Code 为什么总不懂你的项目?缺一张「项目地图」
前端·后端·面试
用户852495071841 天前
Promise 全解析:从三状态到 Promise.all
面试
嘟嘟07171 天前
Node.js spawn 实现迷你 Cursor 自动化终端|子进程完整解析
前端·后端·面试
weedsfly1 天前
纯函数与副作用——前端开发中最容易被忽视的质量保证
前端·javascript·面试
ByWalker_1 天前
web应用技术—MyBatis入门
java·数据库·mybatis·lombok