1. 为什么需要Lambda查询?
第一次用MyBatis-Plus的时候,我最头疼的就是写SQL条件。比如要查年龄大于20岁的用户,传统写法是这样的:
QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.gt("age", 20); // 这里的"age"是数据库字段名这种写法有两个致命问题:第一,字段名是字符串,写错了编译期不会报错;第二,如果数据库字段名改了,所有用到的地方都得手动改。我就在项目里踩过坑,字段名从"age"改成"user_age"后,忘了改某个查询条件,线上直接报错。
Lambda查询就是为了解决这些问题而生的。它用实体类的get方法引用代替字符串,比如:
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.gt(User::getAge, 20); // 编译期就能检查方法是否存在这样就算数据库字段名改了,只要实体类属性名不变,代码就不用改。而且IDE还能自动补全,再也不用担心拼写错误了。
2. Lambda查询的四种姿势
2.1 LambdaQueryWrapper:最正统的写法
这是官方推荐的标准用法,我在团队里也强制要求使用这种方式。创建方法有三种:
// 方式1:直接new(推荐) LambdaQueryWrapper<User> wrapper1 = new LambdaQueryWrapper<>(); // 方式2:从QueryWrapper转换(历史遗留写法) LambdaQueryWrapper<User> wrapper2 = new QueryWrapper<User>().lambda(); // 方式3:使用Wrappers工具类(简洁但不够直观) LambdaQueryWrapper<User> wrapper3 = Wrappers.lambdaQuery();实际查询时,可以链式调用多个条件:
List<User> users = new LambdaQueryWrapper<User>() .like(User::getName, "张") // 名字包含"张" .gt(User::getAge, 18) // 年龄大于18 .orderByDesc(User::getCreateTime) // 按创建时间倒序 .list(userMapper); // 最终执行查询这种写法的优点是清晰明了,适合复杂查询场景。我团队里90%的查询都是用这种方式。
2.2 QueryWrapper.lambda():过渡方案
这是早期版本的写法,现在基本被淘汰了。唯一的使用场景是当你已经有一个QueryWrapper对象,想临时转成Lambda写法:
QueryWrapper<User> queryWrapper = new QueryWrapper<>(); // ...一些非lambda操作 queryWrapper.lambda().eq(User::getName, "张三"); // 中途切换实际项目中不建议混用,容易造成代码风格混乱。我有次review代码就看到有人前半段用字符串字段名,后半段用Lambda,看得人精神分裂。
2.3 Wrappers.lambdaQuery():工具类写法
这是工具类提供的快捷方式:
LambdaQueryWrapper<User> wrapper = Wrappers.lambdaQuery(); wrapper.eq(User::getRole, "admin");这种写法虽然简洁,但可读性稍差。适合在方法内部临时使用,如果是公共方法或者复杂查询,还是建议用标准的LambdaQueryWrapper。
2.4 LambdaQueryChainWrapper:最简洁的链式调用
这是MyBatis-Plus 3.x新增的特性,直接把mapper注入到wrapper里:
List<User> users = new LambdaQueryChainWrapper<>(userMapper) .eq(User::getStatus, 1) .like(User::getNickName, "测试") .list();它的特点是:
- 不需要显式调用mapper方法
- 链式调用到最后自动执行查询
- 适合简单的单表查询
但要注意,复杂查询(比如包含子查询、多表关联)还是用传统的LambdaQueryWrapper更合适。我在处理一个多表join的需求时,强行用ChainWrapper写,结果代码反而更难读了。
3. 实战中的选择策略
3.1 简单查询:LambdaQueryChainWrapper
对于单表的基础CRUD,ChainWrapper能让代码更简洁。比如根据ID查详情:
public User getUserById(Long id) { return new LambdaQueryChainWrapper<>(userMapper) .eq(User::getId, id) .one(); }但要注意它不支持分页,需要分页时还是得用传统写法:
Page<User> page = new Page<>(1, 10); LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getDeptId, deptId); userMapper.selectPage(page, wrapper);3.2 复杂查询:LambdaQueryWrapper
当查询条件需要动态拼接时,LambdaQueryWrapper更灵活。比如这个多条件搜索:
public List<User> searchUsers(UserQuery query) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); if (StringUtils.isNotBlank(query.getName())) { wrapper.like(User::getName, query.getName()); } if (query.getMinAge() != null) { wrapper.ge(User::getAge, query.getMinAge()); } if (query.getMaxAge() != null) { wrapper.le(User::getAge, query.getMaxAge()); } return userMapper.selectList(wrapper); }3.3 团队规范建议
经过多个项目实践,我总结出以下规范:
- 禁止直接使用QueryWrapper(即字符串字段名写法)
- 简单查询可以用LambdaQueryChainWrapper
- 复杂查询、公共方法必须用LambdaQueryWrapper
- 动态SQL条件用LambdaQueryWrapper更清晰
- 分页查询只能用LambdaQueryWrapper
4. 性能优化技巧
4.1 避免重复创建Wrapper
我看到很多同事会在循环里创建Wrapper,这是性能杀手:
// 错误示范 for (Long id : idList) { User user = new LambdaQueryChainWrapper<>(userMapper) .eq(User::getId, id) .one(); // ... } // 正确做法 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); for (Long id : idList) { wrapper.clear(); // 清空上次的条件 wrapper.eq(User::getId, id); User user = userMapper.selectOne(wrapper); // ... }4.2 选择性查询字段
默认select *会查询所有字段,可以用select指定需要的字段:
List<User> users = new LambdaQueryWrapper<User>() .select(User::getId, User::getName) // 只查id和name .eq(User::getStatus, 1) .list(userMapper);这个技巧在查询大字段(如text类型的content)时特别有用,能显著减少数据传输量。
4.3 条件优先级控制
遇到or条件时要注意括号问题:
// 查询状态为1,或者(名字包含张且年龄大于18) wrapper.eq(User::getStatus, 1) .or(qw -> qw.like(User::getName, "张").gt(User::getAge, 18));生成的SQL会是:status = 1 OR (name LIKE '%张%' AND age > 18)。如果不加or嵌套,条件优先级会出错。
5. 常见坑点记录
5.1 日期范围查询
处理日期时要特别注意时区问题:
// 查询今天创建的用户 LocalDateTime start = LocalDate.now().atStartOfDay(); LocalDateTime end = LocalDate.now().plusDays(1).atStartOfDay(); wrapper.between(User::getCreateTime, start, end);我遇到过因为服务器时区设置不同,导致本地测试正常但线上查询结果不对的情况。建议所有日期处理都明确指定时区。
5.2 null值处理
eq(null)会被忽略,要用isNull:
// 查询email为null的记录 wrapper.isNull(User::getEmail); // 查询email不为null的记录 wrapper.isNotNull(User::getEmail);5.3 批量操作
批量insert时,如果用了ChainWrapper要注意:
// 错误:这样只会插入最后一条 new LambdaQueryChainWrapper<>(userMapper) .set(User::getName, "name1").insert() .set(User::getName, "name2").insert(); // 正确:每次都要新建wrapper new LambdaQueryChainWrapper<>(userMapper) .set(User::getName, "name1").insert(); new LambdaQueryChainWrapper<>(userMapper) .set(User::getName, "name2").insert();6. 复杂查询示例
6.1 嵌套查询
查询部门下所有用户:
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.inSql(User::getDeptId, "SELECT id FROM dept WHERE parent_id = " + parentId);6.2 联表查询
虽然MyBatis-Plus主打单表操作,但也能支持联表:
@Select("SELECT u.* FROM user u LEFT JOIN dept d ON u.dept_id=d.id ${ew.customSqlSegment}") List<User> selectUserWithDept(@Param(Constants.WRAPPER) LambdaQueryWrapper<User> wrapper); // 调用时 List<User> users = userMapper.selectUserWithDepp( new LambdaQueryWrapper<User>() .eq(User::getStatus, 1) .like(User::getName, "张") );6.3 动态排序
前端传排序字段时,可以这样安全处理:
public List<User> getUsers(String sortField, boolean asc) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); // 防止SQL注入,只允许排序实体类有的字段 try { Method method = User.class.getMethod("get" + sortField.substring(0,1).toUpperCase() + sortField.substring(1)); if (asc) { wrapper.orderByAsc(User::getMethod(method)); } else { wrapper.orderByDesc(User::getMethod(method)); } } catch (Exception e) { // 默认排序 wrapper.orderByDesc(User::getCreateTime); } return userMapper.selectList(wrapper); }