告别手写SQL!用mybatis-plus-join搞定SpringBoot项目里的多表联查(附完整代码)
告别手写SQL!用mybatis-plus-join轻松实现SpringBoot多表联查
每次看到项目里那些动辄几十行的SQL语句,我就忍不住皱眉——字段名拼写错误、表别名混乱、条件判断重复...这些手工编写的SQL不仅维护困难,还容易成为性能瓶颈。在SpringBoot项目中,我们明明已经用上了MyBatis-Plus这样的ORM框架,为什么还要忍受这种原始操作?
1. 为什么我们需要mybatis-plus-join
传统MyBatis-Plus在处理单表CRUD时表现出色,但遇到多表关联查询就力不从心。我曾在一个用户权限系统中,为了获取用户及其角色信息,不得不编写如下SQL:
SELECT u.*, r.role_name FROM user u LEFT JOIN user_role_mapping ur ON u.id = ur.user_id LEFT JOIN role r ON ur.role_id = r.id WHERE u.status = 1这种代码存在三个致命问题:
- 难以维护:字段硬编码,表结构变更需要全局搜索替换
- 类型不安全:编译器无法检查字段名是否正确
- 可读性差:复杂的JOIN条件嵌套在字符串中
mybatis-plus-join的出现完美解决了这些痛点。它基于MyBatis-Plus进行扩展,提供了一套类型安全的链式API,让我们可以用Java代码优雅地表达多表关联逻辑。
2. 快速集成mybatis-plus-join
2.1 环境准备
新建一个SpringBoot 2.7.x项目,在pom.xml中添加以下依赖:
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>com.github.yulichang</groupId> <artifactId>mybatis-plus-join-boot-starter</artifactId> <version>1.4.7</version> </dependency>提示:建议保持mybatis-plus和mybatis-plus-join版本匹配,避免兼容性问题
2.2 基础配置
在application.yml中配置数据源和插件:
mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mybatis-plus-join: table-alias: t # 默认表别名 sub-table-logic: true # 自动应用逻辑删除3. 两种Wrapper实战对比
mybatis-plus-join提供了两种Wrapper来实现关联查询,各有适用场景。
3.1 MPJLambdaWrapper:类型安全的优雅选择
适合实体类关系明确的情况,编译器会检查字段引用:
public List<UserRoleDTO> getUserRoles(Long userId) { return userMapper.selectJoinList(UserRoleDTO.class, new MPJLambdaWrapper<User>() .select(User::getId, User::getName) .selectAs(Role::getRoleName, UserRoleDTO::getRoleName) .leftJoin(UserRole.class, UserRole::getUserId, User::getId) .leftJoin(Role.class, Role::getId, UserRole::getRoleId) .eq(User::getId, userId)); }优势:
- 自动表别名管理
- 编译时字段检查
- 智能处理逻辑删除
3.2 MPJQueryWrapper:灵活应对复杂场景
当需要处理动态表名或特殊SQL函数时更适用:
public List<UserRoleDTO> getUserRoles(String roleName) { return userMapper.selectJoinList(UserRoleDTO.class, new MPJQueryWrapper<User>() .select("u.id, u.name, r.role_name as roleName") .leftJoin("user_role ur ON u.id = ur.user_id") .leftJoin("role r ON ur.role_id = r.id") .like("r.role_name", roleName)); }适用场景:
- 需要自定义表别名
- 使用数据库特定函数
- 处理动态表名
4. 高级查询技巧
4.1 嵌套查询处理
实现"查询有管理员权限的用户":
List<User> admins = userMapper.selectJoinList(User.class, new MPJLambdaWrapper<User>() .selectAll(User.class) .innerJoin(UserRole.class, UserRole::getUserId, User::getId) .innerJoin(Role.class, Role::getId, UserRole::getRoleId) .exists("SELECT 1 FROM admin_privilege ap WHERE ap.role_id = t2.id") );4.2 分组聚合统计
统计每个角色的用户数量:
@Getter @Setter class RoleUserCountDTO { private String roleName; private Integer userCount; } List<RoleUserCountDTO> stats = roleMapper.selectJoinList(RoleUserCountDTO.class, new MPJLambdaWrapper<Role>() .select(Role::getRoleName) .selectCount(User::getId, RoleUserCountDTO::getUserCount) .leftJoin(UserRole.class, UserRole::getRoleId, Role::getId) .leftJoin(User.class, User::getId, UserRole::getUserId) .groupBy(Role::getId) );4.3 性能优化建议
选择性投影:只查询需要的字段
.select(User::getId, User::getName) // 避免selectAll()合理使用JOIN类型:
- INNER JOIN:默认选择,性能最好
- LEFT JOIN:需要保留主表记录时使用
索引检查:确保关联字段有索引
5. 真实项目中的最佳实践
在电商系统中,我们需要展示订单详情,包含商品信息、用户信息和物流信息。传统方式需要编写复杂SQL,而使用mybatis-plus-join可以这样实现:
public Page<OrderDetailVO> getOrderDetails(PageParam param) { return orderMapper.selectJoinPage(new Page<>(param.getPage(), param.getSize()), OrderDetailVO.class, new MPJLambdaWrapper<Order>() .selectAll(Order.class) .selectAs(User::getName, OrderDetailVO::getUserName) .selectAs(Product::getTitle, OrderDetailVO::getProductName) .selectAs(Logistics::getStatus, OrderDetailVO::getLogisticsStatus) .leftJoin(User.class, User::getId, Order::getUserId) .leftJoin(OrderItem.class, OrderItem::getOrderId, Order::getId) .leftJoin(Product.class, Product::getId, OrderItem::getProductId) .leftJoin(Logistics.class, Logistics::getOrderId, Order::getId) .eq(Order::getStatus, 1) .orderByDesc(Order::getCreateTime)); }项目经验:
- VO类字段建议使用基本类型,避免包装类型的NPE问题
- 复杂查询建议分页处理,避免内存溢出
- 对于超过5表的关联,考虑拆分为多个查询
