1. QueryWrapper基础:从SQL到Java的思维转换
第一次接触QueryWrapper时,我盯着SQL语句看了半小时——明明一行SQL能搞定的事,为什么要用Java代码重新实现?直到在项目里处理第3个需求变更时,我才真正体会到它的价值。想象一下:当产品经理第5次调整查询条件时,你只需要修改几行链式调用的Java代码,而不是在XML里小心翼翼地拼接SQL字符串。
QueryWrapper的本质是用面向对象的方式描述SQL查询。比如这个简单的SQL:
SELECT * FROM user WHERE age > 18 AND name LIKE '张%'用QueryWrapper实现是这样的:
QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.gt("age", 18) .likeRight("name", "张");这里有两个关键点需要注意:
- 条件组合的链式调用:每个条件方法都返回Wrapper对象本身,形成流畅接口(Fluent Interface)
- 数据库字段与Java属性的映射:默认使用驼峰转下划线规则,也可以通过@TableField注解自定义
我在实际项目中踩过的坑是:当字段名包含SQL关键字时(比如order、desc),必须用反引号包裹:
wrapper.select("`order`", "`desc`"); // 正确 wrapper.select("order", "desc"); // 报错!2. 一对一关联查询实战
处理银行用户和银行卡的关系时,我最初的做法是分别查询两个表然后手动组装对象——直到发现N+1查询问题。后来改用QueryWrapper的JOIN查询,性能直接提升8倍。来看这个典型场景:
SQL原貌:
SELECT u.id, u.name, c.card_number FROM user u LEFT JOIN card c ON u.id = c.user_id WHERE u.id = 123Java实现方案:
首先定义包含关联关系的实体类:
public class User { private Long id; private String name; @TableField(exist = false) // 标记非数据库字段 private Card card; } public class Card { private String cardNumber; private Long userId; // 外键字段 }然后使用QueryWrapper构建查询:
QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("u.id", "u.name", "c.card_number AS cardNumber") .eq("u.id", 123) .last("LEFT JOIN card c ON u.id = c.user_id"); User user = userService.getOne(wrapper);这里有几个实用技巧:
- 字段别名映射:SQL中的
c.card_number AS cardNumber会自动映射到User.card.cardNumber属性 - last()方法:用于追加任意SQL片段,但要小心SQL注入风险
- 结果自动封装:MyBatis-Plus会自动处理嵌套对象关系
3. 一对多查询的三种实现方式
当用户有多张银行卡时,情况就变得复杂了。我经历过三种方案迭代:
方案一:多次查询(新手常用)
// 先查用户 User user = userService.getById(123); // 再查卡片 List<Card> cards = cardService.list( new QueryWrapper<Card>().eq("user_id", user.getId()) ); user.setCards(cards);问题:产生N+1查询,性能差
方案二:XML自定义SQL(传统方案)
<select id="getUserWithCards" resultMap="userWithCards"> SELECT * FROM user u LEFT JOIN card c ON u.id = c.user_id WHERE u.id = #{id} </select>缺点:需要维护XML文件,类型安全无法保证
方案三:QueryWrapper+ResultMap(推荐)
// 实体类增加集合字段 public class User { @TableField(exist = false) private List<Card> cards; } // 查询构建 QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("u.*", "c.id AS card_id", "c.card_number") .eq("u.id", 123) .last("LEFT JOIN card c ON u.id = c.user_id"); // 需要自定义结果处理器 List<User> users = userService.list(wrapper); users.forEach(user -> { List<Card> cards = ... // 从结果集中提取卡片数据 user.setCards(cards); });关键点在于:
- 查询时获取所有关联数据
- 在内存中完成数据组装
- 使用MyBatis的结果处理器(ResultHandler)可以优化这个过程
4. 多对多关系的中间表处理
电商系统中的用户-商品收藏关系是典型的多对多场景。经过三个项目的迭代,我总结出这套标准处理流程:
数据库结构:
user (id, name) product (id, title) user_product (user_id, product_id, create_time)Java实体设计:
public class User { @TableField(exist = false) private List<UserProduct> userProducts; } public class UserProduct { private Long userId; private Long productId; private LocalDateTime createTime; @TableField(exist = false) private Product product; }查询构建技巧:
QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.select("u.*", "up.create_time", "p.id AS product_id", "p.title AS product_title") .eq("u.id", 123) .last("LEFT JOIN user_product up ON u.id = up.user_id " + "LEFT JOIN product p ON up.product_id = p.id"); List<User> users = userService.list(wrapper);结果处理优化:
Map<Long, User> userMap = users.stream() .collect(Collectors.toMap(User::getId, Function.identity())); users.forEach(user -> { List<UserProduct> ups = new ArrayList<>(); // 解析结果集填充ups user.setUserProducts(ups); });这种方案的优点是:
- 一次查询获取所有数据
- 内存组装效率高
- 支持复杂的分页查询
5. 动态条件构建技巧
在开发后台管理系统时,我经常需要处理这样的需求:"根据用户输入的任意条件组合查询"。QueryWrapper的动态构建能力在这里大放异彩:
基础版:
QueryWrapper<User> wrapper = new QueryWrapper<>(); if (StringUtils.isNotBlank(name)) { wrapper.like("name", name); } if (startDate != null) { wrapper.ge("create_time", startDate); }Lambda进阶版:
wrapper.lambda() .eq(Objects.nonNull(id), User::getId, id) .like(StringUtils.isNotBlank(name), User::getName, name) .between(Objects::nonNull, User::getCreateTime, startDate, endDate);复杂逻辑处理:
wrapper.and(qw -> qw .gt("age", 18) .or() .isNotNull("vip_level")) .nested(qw -> qw .like("address", "北京") .or() .like("address", "上海"));对应生成的SQL:
WHERE (age > 18 OR vip_level IS NOT NULL) AND (address LIKE '%北京%' OR address LIKE '%上海%')6. 性能优化实战经验
在用户量突破百万后,我们遇到了严重的查询性能问题。通过以下优化手段,最终将查询耗时从1200ms降到80ms:
索引提示:
wrapper.last("USE INDEX(idx_user_phone)");查询字段控制:
// 坏实践 wrapper.select("*"); // 好实践 wrapper.select("id", "name", "phone");批量查询优化:
// 原始方式(产生N条SQL) userIds.forEach(id -> { userService.getById(id); }); // 优化方式(1条SQL) userService.listByIds(userIds);分页查询陷阱:
// 低效写法 wrapper.last("LIMIT 10000, 10"); // 高效写法(基于游标) wrapper.gt("id", lastMaxId) .last("LIMIT 10");这些优化手段配合数据库索引,能让查询性能提升10倍以上。记得在测试环境用EXPLAIN验证执行计划,我曾经因为漏加索引导致生产环境查询超时。