MyBatis-Plus Lambda查询全解析:从QueryWrapper到LambdaQueryChainWrapper的演进与实战
1. 为什么需要Lambda查询第一次用MyBatis-Plus的时候我最头疼的就是写SQL条件。比如要查年龄大于20岁的用户传统写法是这样的QueryWrapperUser wrapper new QueryWrapper(); wrapper.gt(age, 20); // 这里的age是数据库字段名这种写法有两个致命问题第一字段名是字符串写错了编译期不会报错第二如果数据库字段名改了所有用到的地方都得手动改。我就在项目里踩过坑字段名从age改成user_age后忘了改某个查询条件线上直接报错。Lambda查询就是为了解决这些问题而生的。它用实体类的get方法引用代替字符串比如LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.gt(User::getAge, 20); // 编译期就能检查方法是否存在这样就算数据库字段名改了只要实体类属性名不变代码就不用改。而且IDE还能自动补全再也不用担心拼写错误了。2. Lambda查询的四种姿势2.1 LambdaQueryWrapper最正统的写法这是官方推荐的标准用法我在团队里也强制要求使用这种方式。创建方法有三种// 方式1直接new推荐 LambdaQueryWrapperUser wrapper1 new LambdaQueryWrapper(); // 方式2从QueryWrapper转换历史遗留写法 LambdaQueryWrapperUser wrapper2 new QueryWrapperUser().lambda(); // 方式3使用Wrappers工具类简洁但不够直观 LambdaQueryWrapperUser wrapper3 Wrappers.lambdaQuery();实际查询时可以链式调用多个条件ListUser users new LambdaQueryWrapperUser() .like(User::getName, 张) // 名字包含张 .gt(User::getAge, 18) // 年龄大于18 .orderByDesc(User::getCreateTime) // 按创建时间倒序 .list(userMapper); // 最终执行查询这种写法的优点是清晰明了适合复杂查询场景。我团队里90%的查询都是用这种方式。2.2 QueryWrapper.lambda()过渡方案这是早期版本的写法现在基本被淘汰了。唯一的使用场景是当你已经有一个QueryWrapper对象想临时转成Lambda写法QueryWrapperUser queryWrapper new QueryWrapper(); // ...一些非lambda操作 queryWrapper.lambda().eq(User::getName, 张三); // 中途切换实际项目中不建议混用容易造成代码风格混乱。我有次review代码就看到有人前半段用字符串字段名后半段用Lambda看得人精神分裂。2.3 Wrappers.lambdaQuery()工具类写法这是工具类提供的快捷方式LambdaQueryWrapperUser wrapper Wrappers.lambdaQuery(); wrapper.eq(User::getRole, admin);这种写法虽然简洁但可读性稍差。适合在方法内部临时使用如果是公共方法或者复杂查询还是建议用标准的LambdaQueryWrapper。2.4 LambdaQueryChainWrapper最简洁的链式调用这是MyBatis-Plus 3.x新增的特性直接把mapper注入到wrapper里ListUser users new LambdaQueryChainWrapper(userMapper) .eq(User::getStatus, 1) .like(User::getNickName, 测试) .list();它的特点是不需要显式调用mapper方法链式调用到最后自动执行查询适合简单的单表查询但要注意复杂查询比如包含子查询、多表关联还是用传统的LambdaQueryWrapper更合适。我在处理一个多表join的需求时强行用ChainWrapper写结果代码反而更难读了。3. 实战中的选择策略3.1 简单查询LambdaQueryChainWrapper对于单表的基础CRUDChainWrapper能让代码更简洁。比如根据ID查详情public User getUserById(Long id) { return new LambdaQueryChainWrapper(userMapper) .eq(User::getId, id) .one(); }但要注意它不支持分页需要分页时还是得用传统写法PageUser page new Page(1, 10); LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.eq(User::getDeptId, deptId); userMapper.selectPage(page, wrapper);3.2 复杂查询LambdaQueryWrapper当查询条件需要动态拼接时LambdaQueryWrapper更灵活。比如这个多条件搜索public ListUser searchUsers(UserQuery query) { LambdaQueryWrapperUser 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更清晰分页查询只能用LambdaQueryWrapper4. 性能优化技巧4.1 避免重复创建Wrapper我看到很多同事会在循环里创建Wrapper这是性能杀手// 错误示范 for (Long id : idList) { User user new LambdaQueryChainWrapper(userMapper) .eq(User::getId, id) .one(); // ... } // 正确做法 LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); for (Long id : idList) { wrapper.clear(); // 清空上次的条件 wrapper.eq(User::getId, id); User user userMapper.selectOne(wrapper); // ... }4.2 选择性查询字段默认select *会查询所有字段可以用select指定需要的字段ListUser users new LambdaQueryWrapperUser() .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 嵌套查询查询部门下所有用户LambdaQueryWrapperUser 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_idd.id ${ew.customSqlSegment}) ListUser selectUserWithDept(Param(Constants.WRAPPER) LambdaQueryWrapperUser wrapper); // 调用时 ListUser users userMapper.selectUserWithDepp( new LambdaQueryWrapperUser() .eq(User::getStatus, 1) .like(User::getName, 张) );6.3 动态排序前端传排序字段时可以这样安全处理public ListUser getUsers(String sortField, boolean asc) { LambdaQueryWrapperUser 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); }