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()时,如果没有给UpdateWrapper或QueryWrapper设置条件,生成的 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上冲突。 -
解决方案:
-
将
deleted字段也加入唯一索引中(如UNIQUE KEY uk_username (username, deleted)),但需要确保deleted的默认值(0)和删除后的值(1)能区分开。 -
使用不同的删除标识,例如,不使用 0/1,而是使用 0 和主键 ID(或其他唯一值),这样
(username, deleted)的组合永远是唯一的。
-
坑点10:大字段查询导致的性能问题
-
场景 : 实体类中有一个
String content字段对应TEXT类型。在普通的list()查询时,会把这个大字段也查出来,占用大量网络和内存。 -
解决方案:
-
使用
@TableField(select = false)注解,默认不查询该字段。 -
在需要的时候,使用
QueryWrapper的select()方法显式指定需要查询的字段。
-
第二部分:中大厂面试题
中大型互联网公司不仅会问怎么用,更会问 "为什么这么用" 和 "如何优化"。
1. 基础与原理
-
MyBatis-Plus 和 MyBatis 有什么区别?它主要解决了哪些痛点?
-
考察点: 对 MP 定位的理解。
-
参考答案: MP 是 MyBatis 的增强工具,只做增强不做改变。核心解决了:1)通用 CRUD 的样板代码问题;2)强大的条件构造器;3)分页、性能分析等常用功能的插件化;4)代码生成器等。
-
-
请讲讲 MyBatis-Plus 的条件构造器(Wrapper)的工作原理?
-
考察点: 对源码的初步了解。
-
参考答案 : Wrapper 内部维护了一个
List<Segment>集合(SQL 片段),如eq,like等方法都会向这个集合添加一个片段。在最终执行 SQL 时,通过getCustomSqlSegment方法将这些片段按照一定的逻辑(例如,处理AND、OR,过滤null值等)拼接成完整的WHERE子句。
-
-
MyBatis-Plus 是如何实现分页的?它的分页插件原理是什么?
-
考察点: 插件机制和分页实现。
-
参考答案 : 通过实现 MyBatis 的
Interceptor接口。在Executor执行 SQL 前进行拦截。首先,查询总数(通过解析原始 SQL,拼接COUNT(1)的查询);然后,对原始 SQL 根据不同的数据库方言拼接LIMIT等分页关键字。最后执行两条 SQL,并将结果设置到Page对象中。
-
2. 设计与扩展
-
如果让你设计一个多租户(SaaS)系统,如何使用 MyBatis-Plus 实现数据隔离?
-
考察点: 对 MP 插件机制和实际业务场景的结合能力。
-
参考答案 : 使用 MP 的
TenantLineInnerInterceptor租户插件。实现TenantLineHandler接口,在其中指定租户 ID 的字段名(如tenant_id)和如何获取当前租户的 ID(通常从 ThreadLocal 或 Security Context 中获取)。该插件会自动在所有涉及查询、更新、删除的 SQL 上追加tenant_id = ?条件。
-
-
如何基于 MyBatis-Plus 实现一个通用的"数据权限"功能?
-
考察点: 更复杂的插件和 SQL 改写能力。
-
参考答案 : 这也是通过自定义拦截器实现。可以定义一个注解,标注在 Mapper 方法上,指明需要的权限规则(如
{dept_id} = #{user.deptId})。在拦截器中,解析该注解,并根据当前登录用户的信息,动态地将权限规则拼接至 SQL 的WHERE条件中。这比多租户更复杂,因为规则是动态多样的。
-
-
MyBatis-Plus 的代码生成器原理是什么?如果让你定制一个,你会怎么做?
-
考察点: 对代码生成和模板引擎的理解。
-
参考答案 : MP 的代码生成器底层使用了 Apache Velocity 模板引擎。它通过 JDBC 读取数据库的元数据(表结构、字段、注释等),然后将这些数据填充到预设的
.vm模板文件中,从而生成 Entity、Mapper、Service 等代码。如果要定制,可以修改这些模板文件,或者继承AutoGenerator并重写相关方法。
-
3. 陷阱与优化
-
在使用 MyBatis-Plus 的过程中,你遇到过哪些性能问题?是如何优化的?
-
考察点: 实战经验和问题排查能力。
-
参考答案:
-
N+1 问题 : 虽然 MP 提供了
@One和@Many注解,但在复杂场景下容易导致 N+1 查询。优化方式是手动编写连表查询的 SQL,而不是依赖自动映射。 -
大字段查询 : 如前所述,使用
select控制查询字段。 -
全表更新: 强调使用攻击阻断插件。
-
逻辑删除与索引: 强调唯一索引的问题和解决方案。
-
-
-
@CacheNamespace注解和 MyBatis-Plus 的二级缓存机制你了解吗?在分布式环境下有什么坑?-
考察点: 对缓存和分布式系统的理解。
-
参考答案 : MyBatis 原生支持二级缓存,默认是单机版的 PerpetualCache。在分布式环境下,如果多个应用实例同时操作数据库,会导致缓存数据不一致。解决方案是使用集中式缓存,如 Redis,并通过
MybatisRedisCache等实现类来替换默认的缓存实现。
-
总结
| 类别 | 核心要点 |
|---|---|
| 常见坑点 | 注解映射、主键策略、条件构造器判空、模糊查询、全表操作、分页配置、逻辑删除冲突 |
| 面试重点 | 原理 (Wrapper、分页插件)、设计 (多租户、数据权限)、优化 (N+1、缓存)、源码(代码生成器) |
要学好 MyBatis-Plus,不仅要会用其便捷的 API,更要理解其背后的 MyBatis 原理、插件机制,并时刻关注性能和安全问题。在面试中,结合具体业务场景来阐述你的理解和解决方案,会大大加分。