【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 原理、插件机制,并时刻关注性能和安全问题。在面试中,结合具体业务场景来阐述你的理解和解决方案,会大大加分。

相关推荐
卷到起飞的数分2 小时前
5.MyBatis持久(dao)层框架
java·数据库·mybatis
一只叫煤球的猫2 小时前
从 JDK1.2 到 JDK21:ThreadLocal的进化解决了什么问题
java·后端·面试
一点事3 小时前
ruoyi:集成mybatisplus实现mybatis增强
java·开发语言·mybatis
随风飘的云3 小时前
synchronized 的底层原理及优化机制
面试
绝无仅有3 小时前
面试日志elk之ES数据查询与数据同步
后端·面试·架构
绝无仅有4 小时前
大场面试之最终一致性与分布式锁
后端·面试·架构
踏浪无痕7 小时前
@Transactional做不到的5件事,我用这6种方法解决了
spring boot·后端·面试
Dreamboat-L8 小时前
IDEA中在springboot项目中整合Mybatis时@Autowired时,提示Could not autowire解决方案
spring boot·intellij-idea·mybatis
yoke菜籽8 小时前
面试150——区间
面试·职场和发展