QueryWrapper实战:优雅处理数据库关联查询的三种关系

张开发
2026/4/17 19:09:26 15 分钟阅读

分享文章

QueryWrapper实战:优雅处理数据库关联查询的三种关系
1. QueryWrapper关联查询基础认知第一次接触MyBatisPlus的QueryWrapper时我完全被它简洁的链式调用惊艳到了。记得当时项目里有个紧急需求要改十几张表的联查SQL传统XML方式改得我头皮发麻。直到同事推荐了QueryWrapper才发现原来Java代码写联查可以这么优雅。QueryWrapper本质上是个查询条件构造器它把SQL语句拆解成一个个方法调用。比如where条件对应eq()/like()联表对应leftJoin()字段映射对应select()。这种设计特别符合Java开发者的思维习惯不用在XML和Java文件之间来回切换。但真正让我决定全面采用QueryWrapper的是它处理关联查询的能力。传统方式处理表关联要写大量重复的join语句而QueryWrapper通过lambda表达式和对象关系映射把联查逻辑抽象成了可复用的组件。上周我重构了一个包含5张表的查询代码量直接减少了60%。2. 一对一关系实战用户与银行卡最近做金融项目时遇到个典型场景每个用户只能绑定一张主银行卡。数据库设计上bank_user表通过user_id与bank_card表关联。用原生SQL写联查是这样的SELECT u.*, c.* FROM bank_user u LEFT JOIN bank_card c ON u.id c.user_id WHERE u.id 1改用QueryWrapper后代码清爽多了QueryWrapperBankUser wrapper new QueryWrapper(); wrapper.select(u.id, u.name, c.card_number) .eq(u.id, 1L) .leftJoin(bank_card c, u.id c.user_id);这里有个实际开发中的坑要注意当使用select()指定字段时如果两表有同名字段比如都有create_time必须用AS重命名否则MyBatis映射时会覆盖值。我吃过这个亏调试了半天才发现数据错乱的原因。对象映射方面建议在BankUser实体类中添加BankCard属性public class BankUser { private Long id; private String name; TableField(exist false) private BankCard mainCard; // 一对一关联 }这样查询结果会自动映射到mainCard属性。有个小技巧如果联查字段很多可以用ResultMap注解自定义映射规则避免写大量setter代码。3. 一对多关系实战用户与订单电商系统最常见的场景就是用户与订单的一对多关系。假设一个用户可以有多笔订单传统SQL会这样写SELECT u.*, o.* FROM user u LEFT JOIN order o ON u.id o.user_id WHERE u.id 1用QueryWrapper的lambda写法更符合面向对象思维QueryWrapperUser wrapper new QueryWrapper(); wrapper.select(u.id, u.name, o.order_no) .eq(u.id, 1L) .lambda() .leftJoin(Order.class, Order::getUserId, User::getId);实体类设计时要注意public class User { private Long id; private String name; TableField(exist false) private ListOrder orders; // 一对多关联 }这里有个性能优化点默认的联查会返回所有关联记录如果订单量很大建议在wrapper中添加分页参数wrapper.last(LIMIT 10); // 只查最近10笔订单实际项目中我还遇到过N1查询问题先查用户列表再循环查每个用户的订单。解决方案是用QueryWrapper的in()批量查询ListLong userIds userList.stream().map(User::getId).collect(Collectors.toList()); QueryWrapperOrder orderWrapper new QueryWrapper(); orderWrapper.in(user_id, userIds);4. 多对多关系实战用户与角色权限管理系统中的经典多对多关系用户可以有多个角色角色也可以分配给多个用户。这需要通过中间表user_role建立关联。原生SQL查询通常是这样SELECT u.*, r.* FROM user u LEFT JOIN user_role ur ON u.id ur.user_id LEFT JOIN role r ON ur.role_id r.id WHERE u.id 1用QueryWrapper实现更清晰QueryWrapperUser wrapper new QueryWrapper(); wrapper.select(u.id, u.name, r.role_name) .eq(u.id, 1L) .leftJoin(user_role ur, u.id ur.user_id) .leftJoin(role r, ur.role_id r.id);实体类设计需要引入中间实体public class User { private Long id; private String name; TableField(exist false) private ListUserRole userRoles; } public class UserRole { private Long userId; private Long roleId; TableField(exist false) private Role role; }这里有个实际经验多对多查询往往需要额外条件比如只查有效角色。可以在wrapper中添加wrapper.eq(r.status, 1); // 只查询启用状态的角色对于复杂的多对多查询我习惯封装成工具方法。比如这个获取用户所有权限码的方法public ListString getPermissionCodes(Long userId) { QueryWrapperUser wrapper new QueryWrapper(); wrapper.select(p.code) .eq(u.id, userId) .leftJoin(user_role ur, u.id ur.user_id) .leftJoin(role_permission rp, ur.role_id rp.role_id) .leftJoin(permission p, rp.permission_id p.id); // 执行查询并返回结果 }5. 性能优化与常见陷阱经过多个项目实践我总结了一些QueryWrapper联查的优化经验。首先是索引问题所有join条件字段必须建索引特别是多对多关系的中间表。有次线上查询超时排查发现就是漏建了user_role表的联合索引。其次要警惕字段冲突。当多表有相同字段名时建议统一使用AS别名wrapper.select(u.id as userId, r.id as roleId);对于大数据量联查一定要用分页wrapper.last(LIMIT (pageNum-1)*pageSize , pageSize);有个容易踩的坑是NPE问题。当联查结果可能为null时实体类要用Optional包装TableField(exist false) private OptionalBankCard mainCard Optional.empty();日志调试方面建议开启MyBatisPlus的SQL日志mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl最后分享个真实案例有次联查结果始终少字段后来发现是select()方法参数超过了IDE的自动换行限制导致最后几个字段被截断了。现在我都把长select拆分成多行wrapper.select(u.id, u.name, o.order_no, o.create_time, p.product_name);

更多文章