为什么用它
MyBatis 的 XML 写复杂 SQL 很顺手,但日常增删改查里,大量语句其实非常机械:按主键查、按条件查列表、更新几个字段、分页。每个表都手写一遍,XML 会越来越长,改字段时还容易漏。
MyBatis-Plus 在 MyBatis 之上补了通用 Mapper 和条件构造器,单表 CRUD 基本不用写 SQL,复杂查询仍然可以回落到 XML。2023 年新起的项目里,这套组合非常常见。不过它把很多细节藏了起来,用不好会出现"分页没生效""条件越拼越多"这类问题。
版本上要注意:Spring Boot 2.7 用 mybatis-plus-boot-starter;Spring Boot 3 需要 mybatis-plus-spring-boot3-starter(3.5.3 以后提供),引错了启动会报 javax.servlet 相关的类找不到。
条件构造器
最常用的是 LambdaQueryWrapper,用方法引用代替字符串字段名,重构时能跟着改:
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(User.class)
.select(User::getId, User::getUsername, User::getStatus)
.eq(User::getDeleted, 0)
.eq(StringUtils.hasText(req.getStatus()), User::getStatus, req.getStatus())
.and(StringUtils.hasText(req.getKeyword()), w -> w
.like(User::getUsername, req.getKeyword())
.or()
.like(User::getNickname, req.getKeyword()))
.orderByDesc(User::getCreateTime);
List<User> users = userMapper.selectList(wrapper);
几个容易忽略的点:
eq(condition, column, value)的第一个参数是布尔值,条件为false时整段不拼接,适合处理"参数可选"的查询;.and(w -> ...)用来给or加括号,否则or会把前面的条件全部放开,变成"或"关系;.select(...)只查需要的列,避免select *把大字段带出来;- 排序用
orderByDesc(User::getCreateTime),不要用字符串拼字段名,字符串版本有 SQL 注入风险。
分页要装插件
MyBatis-Plus 的分页不是开箱即用的,必须注册拦截器,否则 selectPage 会把所有数据查出来,然后内存里假装分了一下页:
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
PaginationInnerInterceptor pagination =
new PaginationInnerInterceptor(DbType.MYSQL);
pagination.setMaxLimit(500L);
pagination.setOverflow(false);
interceptor.addInnerInterceptor(pagination);
return interceptor;
}
}
maxLimit 限制单页最大条数,防止前端传 pageSize=100000 把数据库拖垮;overflow=false 表示页码超过总页数时返回空列表,而不是回到第一页。
查询时这样用:
Page<User> page = new Page<>(req.getPageNum(), req.getPageSize());
Page<User> result = userMapper.selectPage(page, wrapper);
return new PageResult<>(result.getTotal(), result.getRecords());
分页插件会自动生成两条 SQL:一条 count,一条带 LIMIT 的查询。日志里能看到 LIMIT ?,? 才说明插件生效了。
常见坑
1. 忘了注册分页插件
表现是接口返回了全表数据,total 却是 0 或者等于记录数。先看 SQL 日志里有没有 LIMIT,没有就是拦截器没生效。还要确认拦截器是作为 @Bean 注册的,且没有被其他 MyBatis 配置覆盖。
2. Wrapper 复用导致条件累加
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery();
for (String name : names) {
wrapper.eq(User::getUsername, name); // 每循环一次就多一个 AND
}
同一个 wrapper 多次调用会不断追加条件,第二次查询的结果往往为空。每次查询新建 wrapper,或者用 wrapper.clear()。
3. 逻辑删除和手写 SQL 不一致
实体上加了 @TableLogic,MP 生成的 SQL 会自动带 deleted = 0;但 XML 里手写的 SQL 不会。两边混用时,记得手写语句也补上条件,否则会查出已删除的数据。
4. 唯一索引和逻辑删除冲突
用户名唯一索引包含 deleted 字段时,删除后重新注册同名用户可能失败;索引不含 deleted 时,逻辑删除的数据又占着唯一键。这属于表设计问题,要在建表时想清楚,而不是上线后改。
5. 更新时 null 字段被忽略
updateById 默认只更新非 null 字段。如果业务需要把某个字段置空,得用 UpdateWrapper.set(User::getRemark, null),或者配置全局的字段更新策略。这个行为很多人第一次遇到时会以为"更新没执行"。
6. 批量操作写成了循环单条
for 循环里一条条 insert,一万条数据就是一万次网络往返,慢得离谱。用 saveBatch 或自定义批量插入,并把批次大小控制在几百到一千。注意 saveBatch 底层仍是一条条发 SQL(JDBC 批处理取决于配置),数据量大时最好自己写 insert into ... values (...),(...)。
7. 多表分页的 count 不对
selectPage 配合 join 时,自动生成的 count 可能算错行数,或者 count 语句非常慢。这种情况建议在 XML 里自己写 count 语句,通过 page.setSearchCount(false) 关掉自动 count。
8. 驼峰映射和表名对不上
实体叫 UserInfo,表叫 t_user_info,需要 @TableName("t_user_info")。字段名和列名不一致时用 @TableField。启动后先跑一条查询,确认 SQL 里的表名列名正确,比上线后报错强。
排查步骤
遇到"查询结果不对、分页不对、更新不生效",先打开 SQL 日志:
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
logging:
level:
com.example.mapper: DEBUG
然后按顺序看三件事:
- 最终执行的 SQL 长什么样,条件和
LIMIT有没有拼上; - 参数是不是 null,
eq的条件开关有没有传错; - 返回的
total和records是否一致,count 语句有没有被 join 影响。
如果 SQL 本身没问题,再回头看实体映射和逻辑删除配置。大部分"玄学问题",在 SQL 日志里都能一眼看出来。
评论