MyBatis Plus lambdaQuery 链式查询详解:从入门到进阶
2026/9/9 15:51:44 网站建设 项目流程

做后端开发的同学,尤其是用过 MyBatis Plus 的,应该都有这种经历:原本写一条查询要手工拼 SQL 或者写 XML,字段名一个字母不对就要到运行时才炸出来。后来换了 MyBatis Plus,有了 lambdaQuery(),这个问题基本就消失了。lambdaQuery() 是 MyBatis Plus 提供的一种基于 Lambda 表达式的链式查询构造器,它不用硬编码数据库字段名字符串,而是直接引用实体类的属性方法,编译期就能拦住大部分低级错误。

这个工具能做什么?说白了就是把原本要写在 XML 里的 where 条件、排序、分组、分页,全部改成 Java 代码里一行行链式调用,代码既短又直观。对于 CRUD 为主的业务系统,lambdaQuery() 基本能满足 90% 以上的查询需求。适合刚接触 MP 的新手快速上手,也适合写过一些但还没系统梳理过 lambdaQuery 用法的同学查漏补缺。

这篇文章我就以实际项目的角度,把 lambdaQuery() 常用基础用法拆开讲一遍,顺便把最近网上问得比较多的几个问题也一并解决:批量插入到底怎么用才算对、insert 数据没报错却没写成功是什么原因、逻辑删除的数据怎么再查出来、以及怎样临时禁用逻辑删除条件。内容以 3.x 版本为准,覆盖日常开发最常用的场景。

1. 从 QueryWrapper 到 lambdaQuery():为什么推荐链式查询

1.1 传统写法和 lambda 写法的核心区别

先看一段对比。传统写法用 QueryWrapper,条件字段只能写字符串:

QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.eq("name", "张三"); wrapper.ge("age", 18); List<User> list = userMapper.selectList(wrapper);

这段代码功能上没有错,但问题很明显:“name”和“age”是魔法字符串。一旦实体类属性改名或者数据库字段调整,Java 编译器不会帮你发现,只有跑到这一行才会报错,甚至可能不报错而是查出错误结果。而 lambdaQuery() 是这么写的:

List<User> list = userService.lambdaQuery() .eq(User::getName, "张三") .ge(User::getAge, 18) .list();

User::getName 直接引用实体类的方法引用,字段名在编译期就能校验。如果 User 里没有 getName 这个方法,编译直接失败。这不是什么高深技术,就是 Java 8 方法引用加函数式接口的常规运用,但开发体验提升非常明显。前一种写法写错字段名后,等代码上线跑挂了才发现,后一种写错直接编译不通过,哪个更省事不用多说。

1.2 lambdaQuery() 的入口:BaseMapper 与 IService 的区别

使用 lambdaQuery() 有两个入口,取决于你的代码用到哪一层。如果你用的是 Mapper 层,写法是 wrapper 方式:

LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.eq(User::getName, "张三"); userMapper.selectList(wrapper);

如果你用的是 Service 层(实现了 IService 接口),可以直接链式调用:

userService.lambdaQuery() .eq(User::getName, "张三") .list();

Service 链式写法内部其实就是把条件包装成一个 LambdaQueryWrapper,再调用 mapper 对应方法,本质是同一套东西。日常业务开发,我更推荐 Service 层链式写法,代码短、可读性好。但如果你需要非常复杂的 SQL,还是得退回 wrapper 或者直接写自定义 SQL。这个看项目规范和个人习惯,没有绝对的对错。

1.3 版本和环境准备

以目前主流的 MyBatis Plus 3.5.x 为例,lambdaQuery() 是内置能力,不需要额外引入包。使用前确保 pom.xml 里有:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency>

实体类建议统一加 @TableName 指定表名,主键加 @TableId,避免默认规则映射错。比如:

@Data @TableName("sys_user") public class User { @TableId(type = IdType.AUTO) private Long id; private String name; private Integer age; private Integer status; @TableLogic private Integer deleted; }

这里面 @TableLogic 就是逻辑删除注解,加了它之后,MP 在执行普通查询时会自动追加“deleted=0”条件,这个问题后面我会单独展开。版本这步容易被忽略,但实际挺重要,不同小版本的 lambdaQuery 在某些重载方法上有差异,比如 3.5.3 之后对 select 排除字段的写法做了调整,如果照老版本代码写,编译可能不通过。

2. 核心 API 详解:常用查询方法逐个拆解

2.1 等值与非等值查询

最常用的就是 eq 和 ne,分别对应 SQL 里的 = 和 <>:

// 查询姓名为张三的用户 userService.lambdaQuery() .eq(User::getName, "张三") .list(); // 查询姓名不是张三的用户 userService.lambdaQuery() .ne(User::getName, "张三") .list();

注意 eq 的第二个参数如果传 null,MP 默认会直接忽略这个条件,不会拼进 SQL。这是 MP 的一个设计细节,很多人不知道,后面讲动态条件时会用到。如果不希望它忽略,可以使用 eq 方法的重载形式并显式传入判断条件,这个后面也会说到。等值查询是最基础的查询,几乎所有业务接口里都会有,理解它的“null 忽略”机制,才能更好地利用后面讲到的动态条件拼接。

2.2 模糊查询四兄弟

模糊查询有四个方法:like、notLike、likeLeft、likeRight,分别对应 SQL 里的 LIKE、NOT LIKE、LIKE '%值'、LIKE '值%'。

// 姓名包含"张" userService.lambdaQuery() .like(User::getName, "张") .list(); // 姓名以"张"开头 userService.lambdaQuery() .likeRight(User::getName, "张") .list(); // 姓名以"三"结尾 userService.lambdaQuery() .likeLeft(User::getName, "三") .list();

like 会自动在值两边加 %,生成LIKE '%张%'。likeRight 只在右边加 %,对前缀匹配场景效率更友好。这里有个实际经验:如果表数据量大,尽量不要用 like 做后模糊匹配,因为百分号在前面会让索引失效,写成 likeRight 或者单独给前缀查询建索引会好很多。模糊查询是搜索场景的常客,但这几个方法别乱用,能用 likeRight 就不用 like,能用等值就不用模糊,索引才能真正发挥作用。

2.3 范围查询

范围查询包括 gt、ge、lt、le、between、notBetween:

// 年龄大于等于18 userService.lambdaQuery() .ge(User::getAge, 18) .list(); // 年龄在18到30之间 userService.lambdaQuery() .between(User::getAge, 18, 30) .list();

between 是闭区间,生成 SQL 是age between 18 and 30。如果 begin 和 end 其中一个为 null,between 会自动退化为单边界条件,不生成 between。gt/ge/lt/le 分别对应 >、>=、<、<=,没有任何坑,注意边界条件用对即可。范围查询在价格筛选、日期区间、年龄统计这些场景非常常用,特别是时间范围查询,配合 Date 类型字段使用时要留意时区问题。

2.4 集合查询 in 与 notIn

in 和 notIn 常用于列表筛选,比如根据 id 集合查用户:

List<Long> ids = Arrays.asList(1L, 2L, 3L); userService.lambdaQuery() .in(User::getId, ids) .list();

这里有个性能实操点:in 的集合如果太大,比如几千上万条,生成的 SQL 会很长,而且 MySQL 对 IN 列表长度有优化瓶颈。我的习惯是,如果集合超过 1000,就分批查询再合并,或者改用临时表/关联查询。MP 的 in 方法底层会做空集合判断,如果集合为 null 或空,这个条件会被忽略,不会生成错误的IN ()。这个设计救了不少人,否则空集合直接拼 SQL 会生成IN (),数据库直接报语法错误。

2.5 空值判断 isNull 与 isNotNull

// 查询邮箱为空的用户 userService.lambdaQuery() .isNull(User::getEmail) .list();

这个在实际开发中很常用,比如查询没填邮箱或者手机号的用户做回访。注意 isNull 没有参数,直接传字段引用。isNotNull 用法一样,只是语义相反。有些同学刚上手会想用eq(User::getEmail, null)来查空值,这是错误的,SQL 里判断空值不能用等号,必须用 IS NULL,所以 MP 才单独提供了这两个方法。

2.6 排序与分页

排序是 orderByAsc 和 orderByDesc,可以传多个字段:

userService.lambdaQuery() .orderByDesc(User::getCreateTime) .orderByAsc(User::getId) .list();

生成 SQL 是order by create_time desc, id asc。多字段排序的场景,谁在前谁在后会影响最终顺序,SQL 本身就是从左到右的优先级。分页要用 Page 对象配合 mapper 或者 chain 里的 page 方法。IService 链式写法:

Page<User> result = userService.lambdaQuery() .ge(User::getAge, 18) .page(new Page<>(1, 10));

page(new Page<>(1, 10)) 返回的 Page 对象里有 records(当前页数据)、total(总记录数)、pages(总页数)等。注意 MP 分页必须配置分页插件,否则分页不会生效,只是普通查询。拦截器配置:

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; }

没有分页插件时页面数据能查出来,但 total 永远是 0,这个问题很多人踩过。分页插件是 MP 使用率最高的配置之一,每次新项目搭建我都会第一时间把它加上。

2.7 分组与聚合

分组使用 groupBy 和 having:

userService.lambdaQuery() .groupBy(User::getDeptId) .having("count(*) > {0}", 5) .list();

having 和 groupBy 配合使用,对分组后的结果做过滤。这里的{0}是占位符,MP 会做参数绑定,不要用直接拼接字符串的方式,防止 SQL 注入。不过说实话,真正复杂的聚合查询我不太推荐用 lambdaQuery 写,一是可读性差,二是聚合场景用自定义 SQL 更清晰。lambdaQuery 更合适的是简单分组,比如统计各部门人数这种。

2.8 last、exists、notExists

last 方法可以拼接一段原生 SQL 到查询末尾,比如:

userService.lambdaQuery() .eq(User::getStatus, 1) .last("limit 1") .one();

这里用last("limit 1")而不是查完再取第一条,能少查一些数据。但 last 方法一定要小心,它直接拼接 SQL 片段,如果内容来自外部输入,就有 SQL 注入风险。我的原则是 last 的内容必须写死,绝不能拼用户输入。exists 和 notExists 在 lambdaQuery 里也支持,但参数需要写一个 wrapper,比较复杂。日常场景用得不多,场景复杂时建议直接上 XML,那才是复杂 SQL 的主场。

3. 常用场景拆解:从动态条件到链式组合

3.1 动态条件拼接的两种姿势

业务里最常见的需求是:查询条件可能有,也可能没有。比如搜索框输入了名字就按名字查,没输入就查全部。第一种写法是 if 判断往 wrapper 里加条件:

LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); if (StringUtils.hasText(name)) { wrapper.eq(User::getName, name); } if (age != null) { wrapper.ge(User::getAge, age); } List<User> users = userMapper.selectList(wrapper);

第二种写法是直接利用 condition 参数,这是 eq 等系列方法的重载形式:

List<User> users = userMapper.selectList(Wrappers.<User>lambdaQuery() .eq(StringUtils.hasText(name), User::getName, name) .ge(age != null, User::getAge, age));

第二种写法更紧凑,适合条件不多的情况。两种方式效果一样,看团队规范和个人习惯。我个人在接口参数校验不复杂的场景下,比较喜欢第二种,代码行数少,一眼能看到所有条件。

3.2 链式查询配合 BaseMapper

前面说的 service.lambdaQuery() 是 IService 的实现,需要你的 Service 接口继承 IService。如果你的代码还停留在 Mapper 层,可以直接用 BaseMapper 配合 LambdaQueryWrapper,两种方式混用也没问题:

LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getName, "张三") .or() .eq(User::getEmail, "zhangsan@example.com"); List<User> list = userMapper.selectList(wrapper);

这里的 or() 是一个重点。默认多个条件之间是 and 连接,or() 会把接下来的条件变成 or 连接。如果不加括号,SQL 的运算优先级容易出问题,后面单独讲。Mapper 层写法更接近底层,适合对 SQL 有强控制欲的人;Service 层写法更符合业务封装,适合快速开发。

3.3 and 和 or 的嵌套组合

先说最容易翻车的 or 用法。以这个为例:

userService.lambdaQuery() .eq(User::getName, "张三") .eq(User::getStatus, 1) .or() .eq(User::getAge, 18) .list();

这段代码生成的条件是:name='张三' and status=1 or age=18。由于 and 优先级高于 or,所以实际是(name='张三' and status=1) or age=18,和业务本意“张三且状态正常,或者年龄18岁”刚好一致。但如果你想表达“张三且(状态为1或年龄为18)”,上面的写法就不对了,需要用 and 嵌套:

userService.lambdaQuery() .eq(User::getName, "张三") .and(wrapper -> wrapper .eq(User::getStatus, 1) .or() .eq(User::getAge, 18)) .list();

and(Consumer) 方法接收一个子 wrapper,里面的条件都会用括号包起来。这是 lambdaQuery 里最实用也最容易被忽略的一个方法,遇到 or 条件务必记得用 and 包裹。我见过不少线上查询结果不准确的问题,最后定位下来都是 or 和 and 优先级搞混了,这个坑基本每个用 MP 的人都会踩一次。

3.4 select 字段筛选,避免查出一堆用不到的列

默认查询是 select *,很多业务表字段几十个,全查出来既浪费 IO 又占内存。可以用 select 指定字段:

userService.lambdaQuery() .select(User::getId, User::getName, User::getAge) .list();

生成 SQL 是select id, name, age from sys_user ...。这条习惯很多人没用上,等数据量大起来差距就出来了。还可以用 select 排除指定字段:

userService.lambdaQuery() .select(User.class, info -> !"content".equals(info.getColumn())) .list();

这个写法稍微绕一点,平时我还是更推荐直接列出需要的字段。字段筛选不只是为了省流量,在某些特殊场景下还有安全意义,比如用户表里的密码字段,查询时压根就不应该查出它来,用 select 排除掉比查出后在代码里置 null 更稳妥。

3.5 groupBy 与 having 的一个实际案例

假设要统计每个部门下状态正常的用户数:

userService.lambdaQuery() .select(User::getDeptId, User::getStatus) .eq(User::getStatus, 1) .groupBy(User::getDeptId) .having("count(*) > {0}", 2) .list();

这里 select 处只查了 deptId 和 status,但如果你在 select 里加了普通字段比如 User::getName,MySQL 的 ONLY_FULL_GROUP_BY 模式会直接报错。遇到 group by 相关的报错,先检查 select 字段是否是分组字段或聚合字段。我实际工作中很少在 lambdaQuery 里写 group by,因为一旦涉及多个聚合函数、条件筛选、排序,代码可读性就会变得很差,更推荐用 XML 写,但简单分组用 lambdaQuery 还是没问题的。

3.6 只查第一条记录

one() 和last("limit 1")都行。one() 的语义是“最多返回一条记录”,如果查询结果有多条,MP 会抛 TooManyResultsException,这一点要特别注意。想安全取一条且不报错,两个办法,一是用 last 加 limit:

User user = userService.lambdaQuery() .eq(User::getMobile, "13800000000") .last("limit 1") .one();

二是直接 list 后取 get(0)。移动端查询通常手机号唯一,用 one() 比较安全,唯一索引没建好时再用 limit 兜底。写 one() 之前想一下这个条件在业务上是不是真的唯一,如果不是,就别偷懒。

4. 进阶:批量插入、逻辑删除与“不报错却没写入”的排查

4.1 批量插入的正确用法

网上问“mybatis plus 批量插入”的人很多。MP 的 IService 接口提供了 saveBatch:

List<User> userList = new ArrayList<>(); for (int i = 0; i < 1000; i++) { User user = new User(); user.setName("测试用户" + i); user.setAge(20 + i % 10); userList.add(user); } boolean saved = userService.saveBatch(userList);

saveBatch 默认每批 1000 条,可以传入 batchSize 调整,比如 saveBatch(userList, 500)。它底层会把实体插入语句组合成一条多值 SQL,也就是insert into user(...) values (...),(...),(...),减少网络往返。需要留意的是,saveBatch 走的是 MyBatis 的批量执行器,默认情况下 MySQL JDBC 驱动对批量语句不会自动启用 rewriteBatchedStatements,所以如果想真正提高批量插入效率,可以在 JDBC URL 上加一个参数:

jdbc:mysql://localhost:3306/test?rewriteBatchedStatements=true

加了之后,驱动会把多条 insert 语句重写成一条多值 SQL,性能提升非常明显。如果不加,MP 的批量插入虽然能成功,但速度会差很多。我在本地测过,插入 1 万条数据,加了 rewriteBatchedStatements 之后耗时能缩短到原来的五分之一左右。注意这个参数只对 MySQL 有效,其他数据库需要各自对应的配置。

还有一种情况:如果你的实体类主键是 IdType.ASSIGN_ID 或者 AUTO 都没问题,但如果主键策略设置不对,批量插入可能报Field 'id' doesn't have a default value,最常见的原因就是数据库主键没配自增,但 MP 默认策略配置成 AUTO 了,两者对不上。批量插入的时候尤其明显,因为单条插入即使失败了,排查起来也容易,批量插入一条失败整批回滚,排查成本更高。

4.2 insert 数据没有写成功但也没报错,究竟为什么

这个问题几乎每隔一段时间就会在群里出现一次:“我调了 userMapper.insert(user),控制台也没报错,事务也提交了,但数据库里就是没数据。” 我排查下来最典型的几个原因,按概率排一下:

第一个原因,也是最隐蔽的:实体类没有加 @TableName,MP 根据类名去映射表名,如果映射到一张不存在的表会报错,但如果项目里恰好有一张名字匹配但业务无关的表,比如你插到别的表去了,就会“没报错也没写入目标表”。

第二个原因:SQL 日志没开,你以为执行了,其实 mapper 方法动态代理后走了自定义 SQL 或者被拦截器跳过。这种情况一打开 SQL 日志就清楚了。

第三个原因:事务没提交。如果方法上加了 @Transactional,但外层异常被吞掉了,事务可能在 catch 之后悄然回滚,看起来就像没有写入。

第四个原因:逻辑删除字段没设置。如果表里有 deleted 字段并且实体加了 @TableLogic,插入时如果 deleted 为 null,MP 会默认填 0,这个一般不是问题;但如果有人给逻辑删除字段设了非 0 值,插入后查不出来,误以为没写入。

第五个原因,回滚了但没报错,这个说白了还是事务问题,日志里一般能看到 rollback 信息。

排查这类问题,最快的方式就是把 MyBatis SQL 日志打开,确认 insert 语句真的发出去了、参数是什么、有没有回滚。application.yml 里加:

mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

SQL 一打印,百分之八九十的问题都能看出来。我一直觉得,遇到这种“不报错但结果不对”的问题,第一件事不是猜,而是把日志打开,看真实执行的 SQL 和参数。

4.3 逻辑删除的默认行为:为什么查出来的数据总少一些

加了 @TableLogic 注解之后,MP 会自动拦截 CRUD 操作。查询时自动拼上 deleted=0,删除时自动把 deleted 置为 1 而不是物理 delete。这是一个非常实用的设计,但副作用就是:你想把包括已删除数据在内的全量数据查出来时,普通的 lambdaQuery() 查不到,因为它默认给你过滤了。

网上搜“mybatis plus 查询 禁用逻辑删除”和“mybatis plus 怎么将逻辑删除的数据也查询出来”,其实都是同一个问题:如何绕过逻辑删除过滤。这个问题出现频率很高,因为很多业务有这个需求:管理员后台要看到已删除的数据,统计报表要包含删除前的数据,或者需要恢复被删除的数据。

4.4 三种把逻辑删除数据也查出来的方案

第一种方案,最省事的:自定义 SQL。在 Mapper 里写一条 @Select 注解或者 XML,SQL 自己写,不要用 MP 的方法。MP 拦截逻辑删除只对 MP 内置方法生效,自定义 SQL 不会自动拼接 deleted=0,你可以随意控制查询条件。比如:

@Select("select * from sys_user where name = #{name}") List<User> selectAllIncludeDeleted(@Param("name") String name);

第二种方案:用 XML 写纯 SQL,把 resultType 映射成实体类,这样查出来的数据既能包含已删除的,又能直接映射成 User 对象:

<select id="selectIncludeDeleted" resultType="com.example.entity.User"> select * from sys_user where name = #{name} </select>

第三种方案,也是我比较推荐的:如果只是偶尔一次需要查询已删除数据,直接在自定义 Mapper 方法里绕过即可。如果频繁需要,建议重新审视逻辑删除字段的设计是不是合理,比如可以把删除状态拆成 status 字段,而不是硬用一个 deleted 标志位。逻辑删除本身是一种取舍,它保留了数据可追溯性,但代价就是所有查询都要多一个条件,某些特殊场景还得单独处理。

4.5 临时禁用逻辑删除过滤的一个实测技巧

有人问能不能在单次查询里禁用逻辑删除,MP 3.5.2+ 有 @InterceptorIgnore(tenantLine = "true")、@InterceptorIgnore(dataPermission = "true") 这类注解,但逻辑删除不是拦截器做的,是 SQL 注入器做的,所以这个注解对这种场景基本无效。要想按查询级别控制,官方没有直接参数开关,所以才更多用自定义 SQL 来解决。还有一个思路是自定义一个 Mapper 方法,方法名能看出是“包含已删除”的特殊查询,代码里调用时也一目了然,这样既绕过了逻辑删除,又不会让同事看代码时产生误解。

@Select("select * from sys_user where id = #{id}") User selectByIdIncludeDeleted(@Param("id") Long id);

这段 SQL 也建议在 XML 里写,格式化和维护都更方便。

5. 常见问题与排查技巧实录

5.1 分页插件没配置,total 永远为 0

这个问题放在第一个讲,因为太典型了。很多人用了 page 方法,发现 records 能查出来,但 total = 0,page 总数也是 0,这就是没配置分页插件。MP 3.5.x 的配置方式:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

配置之后,分页才会拼接 limit,total 才会通过 count 查询得到。注意分页插件对多数据源、多数据库类型敏感,DbType 要传对,比如 MySQL 传 DbType.MYSQL,PostgreSQL 传 DbType.POSTGRE_SQL。如果你项目里接了多个数据源,分页插件只对当前数据源生效,这个也要留意。

5.2 字段名映射不上:下划线和驼峰

MP 默认开启下划线转驼峰,也就是数据库字段 create_time 能映射到实体属性 createTime。如果你们公司数据库命名不规范,字段直接叫 createTime,那默认反而映射不上。可以在 application.yml 里调:

mybatis-plus: configuration: map-underscore-to-camel-case: true

还有一种情况:实体属性命名和数据库字段对不上,比如数据库叫 user_name,实体属性叫 name。这就必须在实体字段上加 @TableField:

@TableField("user_name") private String name;

不加注解,查询出来的 name 永远是 null,又因为不报错,排查起来很恼火。当你发现某个字段查出来是 null 但数据库里有值,第一反应就去看实体映射注解。还有一种常见情况,实体里有个属性在数据库表中没有对应字段,不加 @TableField(exist = false) 的话,插入和查询都会报错,这一点新手很容易踩。

5.3 one() 方法在多条记录时直接报错

one() 的语义是“查询一条记录”,如果 SQL 查出了多条,MP 会抛 TooManyResultsException。很多人把 one() 当成“取第一条”来用,这其实不对,取第一条应该用 limit 1 或者 list() 后 get(0)。用 one() 之前,要确保查询条件在业务上唯一,比如主键查询、唯一索引字段查询。如果不确定,就用last("limit 1")或有序分页第一条。我在代码 review 时,看到有人用 one() 取手机号对应的用户,结果生产环境出现了重复数据,直接接口 500,就是因为没加唯一索引还用了 one()。

5.4 N+1 查询问题

lambdaQuery() 写起来很爽,但也容易在循环里出问题,最常见的坑就是在 for 循环里查数据库:

for (Long deptId : deptIds) { List<User> users = userService.lambdaQuery() .eq(User::getDeptId, deptId) .list(); // 处理逻辑 }

这个就是经典的 N+1 查询,每循环一次就发起一次查询,100 个部门就是 101 条 SQL。优化方式很简单:把 deptIds 整个传进去,用 in 一次查出来,再在内存里按部门分组:

List<User> users = userService.lambdaQuery() .in(User::getDeptId, deptIds) .list(); Map<Long, List<User>> userMap = users.stream() .collect(Collectors.groupingBy(User::getDeptId));

性能差异在数据量小的时候不明显,数据量上去之后,循环查库会导致数据库连接被打满、接口响应变慢,所以写 lambdaQuery 时多想想能不能一次查完。这个优化习惯比任何工具技巧都重要,很多时候接口慢不是 MP 的问题,是使用方式的问题。

5.5 SQL 日志与慢 SQL 排查

排查 lambdaQuery 生成的 SQL 是否正确、是否走了索引,第一步都是看日志。除了 StdOutImpl 打印完整 SQL,也可以配合 p6spy 这类工具。但最简单的还是把日志打开,看生成的条件顺序、limit 语句、参数值。另外,lambdaQuery 生成的 SQL 一般都能正常利用索引,但如果有 or 条件、前模糊 like、对索引列做函数操作,索引可能失效。所以遇到慢 SQL,优先用 explain 查看执行计划。MP 也提供了 queryWrapper 的 getSqlSegment 方法,可以在代码里直接打印条件片段:

LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.eq(User::getName, "张三"); System.out.println(wrapper.getSqlSegment());

这个方法在调试时很有用,可以看到 MP 到底帮你拼了什么。不过 getSqlSegment 打印出来的片段里参数是占位符 ?,不是具体值,想看到具体值还是要靠完整 SQL 日志。

5.6 高版本中的一些新方法和变化

MP 3.5.0 之后,BaseMapper 增加了 selectCount 等方法重载,IService 里也增加了 lambdaQuery 的一些重载。另外 3.5.4 后逻辑删除对某些自定义方法的行为也有调整,建议升级版本前先看 release notes。如果项目还是老版本,升级到 3.5.x 后,主要注意分页插件配置方式的迁移,旧版本 PaginationInterceptor 已废弃,统一改用 MybatisPlusInterceptor。整体来说,lambdaQuery() 覆盖了日常 CRUD 查询的绝大部分场景,越用越顺手。但真要追求极致性能或者写复杂的报表 SQL,还是应该老老实实回到 XML。

最后再分享一个我在实际项目里的小技巧:把一些高频查询条件封装成实体类里的静态方法,比如在 User 实体类里写一个构建查询条件的公共方法,业务层直接用,这样每个业务方法下来代码都会短很多。举个例子:

public static LambdaQueryWrapper<User> buildNormalQuery(String name, Integer minAge) { return Wrappers.<User>lambdaQuery() .eq(StringUtils.hasText(name), User::getName, name) .ge(minAge != null, User::getAge, minAge) .orderByDesc(User::getCreateTime); }

调用的时候一行搞定,团队里新同学拿到就能用,也不容易写错条件。做后端开发,工具用熟了,能省下不少排查问题的时间。顺手把日志开关配好、分页插件配好、唯一索引建好,很多坑根本不会

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询