在 MyBatis-Plus(简称 MP)的日常使用中,单表查询通过 LambdaWrapper 就能轻松实现,但面对多表联查场景,很多开发者会陷入 "是否要退回原生 SQL" 的困惑。其实 MP 提供的MPJLambdaWrapper(多表联查 Lambda 封装)能完美解决这一问题,它既保留了 Lambda 表达式的类型安全优势,又支持灵活的多表关联逻辑。本文将从基础到进阶,带大家掌握 MPJLambdaWrapper 多表联查的核心用法。
一、MPJLambdaWrapper 是什么?为什么需要它?
在了解具体用法前,我们先明确MPJLambdaWrapper的定位:它是 MP 扩展的多表查询封装类,基于LambdaWrapper增强而来,专门用于处理多表关联查询场景。
1. 解决传统多表查询的痛点
- 原生 SQL 联查:需手动拼接表名、关联条件,容易出现字段名拼写错误,且无法享受 MP 的字段自动映射优势;
- 普通 LambdaWrapper:仅支持单表操作,无法直接添加表关联逻辑;
- MPJLambdaWrapper:支持通过 Lambda 表达式指定关联表、关联条件、字段映射,兼顾 "类型安全" 与 "开发效率"。
2. 核心优势
- 类型安全:通过实体类 Lambda 表达式指定字段,避免硬编码字段名和表名,减少运行时错误;
- 链式调用:支持与 LambdaWrapper 类似的链式语法,代码可读性更高;
- 灵活映射:可自定义多表查询结果的返回结构,无需依赖固定实体类;
- 兼容 MP 特性:支持分页、条件判断、排序等 MP 原生功能,无缝衔接现有代码。
二、前置准备:环境配置与依赖
使用 MPJLambdaWrapper 前,需确保项目已正确配置 MyBatis-Plus 环境,且引入相关依赖(以 Spring Boot 项目为例)。
1. 核心依赖
在pom.xml中添加 MP 核心依赖(建议使用 3.5.0 + 版本,确保多表联查功能稳定):
XML
<!-- MyBatis-Plus Spring Boot Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<!-- MP多表联查扩展(部分版本需单独引入) -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-extension</artifactId>
<version>3.5.5</version>
</dependency>
2. 实体类与表结构准备
为方便后续案例演示,我们定义两个关联表:
- 用户表(user):id(主键)、username(用户名)、dept_id(部门 ID,外键);
- 部门表(dept):id(主键)、dept_name(部门名称)、status(状态:0 - 禁用,1 - 启用)。
对应的实体类如下:
java
// User实体类
@Data
@TableName("user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private Long deptId; // 关联dept表的id
}
// Dept实体类
@Data
@TableName("dept")
private class Dept {
@TableId(type = IdType.AUTO)
private Long id;
private String deptName;
private Integer status;
}
三、MPJLambdaWrapper 多表联查实战
MPJLambdaWrapper 的核心是通过join方法指定关联表,结合select方法自定义返回字段,最终通过list或page方法执行查询。下面分场景讲解具体用法。
1. 基础场景:两表内连接(INNER JOIN)
需求:查询 "启用状态的部门下的所有用户",返回 "用户名、部门名称"。
实现步骤:
- 创建MPJLambdaWrapper对象,指定主表(User);
- 通过join方法添加关联表(Dept),并设置关联条件(user.dept_id = dept.id);
- 通过eq方法添加过滤条件(dept.status = 1);
- 通过select方法指定返回字段;
- 调用 Mapper 的selectList方法执行查询。
代码示例:
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<Map<String, Object>> getUserWithDept() {
// 1. 创建MPJLambdaWrapper,主表为User
MPJLambdaWrapper<User> wrapper = new MPJLambdaWrapper<>(User.class);
// 2. 内连接Dept表,关联条件:user.deptId = dept.id
wrapper.join(Dept.class, Dept::getId, User::getDeptId)
// 3. 过滤条件:部门状态为启用(1)
.eq(Dept::getStatus, 1)
// 4. 指定返回字段:用户名(user.username)、部门名称(dept.deptName)
.select(User::getUsername, Dept::getDeptName);
// 5. 执行查询,返回Map结构(key为字段名,value为字段值)
return userMapper.selectList(wrapper);
}
}
生成的 SQL 逻辑:
sql
SELECT
user.username,
dept.dept_name
FROM
user
INNER JOIN
dept ON user.dept_id = dept.id
WHERE
dept.status = 1
2. 进阶场景 1:左连接(LEFT JOIN)与分页查询
需求:查询 "所有用户及其所属部门信息(无部门的用户也需显示)",支持分页,返回 "用户 ID、用户名、部门名称"。
关键差异:
- 左连接需通过joinLeft方法指定,而非默认的内连接;
- 分页查询需结合 MP 的IPage对象,通过selectPage方法执行。
代码示例:
java
public IPage<Map<String, Object>> getUserWithDeptPage(Integer pageNum, Integer pageSize) {
// 1. 构建分页对象(pageNum:当前页,pageSize:每页条数)
IPage<User> page = new Page<>(pageNum, pageSize);
// 2. 创建MPJLambdaWrapper,主表为User
MPJLambdaWrapper<User> wrapper = new MPJLambdaWrapper<>(User.class);
// 3. 左连接Dept表,关联条件:user.deptId = dept.id
wrapper.joinLeft(Dept.class, Dept::getId, User::getDeptId)
// 指定返回字段:用户ID、用户名、部门名称
.select(User::getId, User::getUsername, Dept::getDeptName);
// 4. 执行分页查询
return userMapper.selectPage(page, wrapper);
}
生成的 SQL 逻辑:
sql
SELECT
user.id,
user.username,
dept.dept_name
FROM
user
LEFT JOIN
dept ON user.dept_id = dept.id
LIMIT
#{pageNum-1}*#{pageSize}, #{pageSize}
3. 进阶场景 2:多表联查(三表及以上)
需求:新增 "角色表(role)",查询 "用户 - 部门 - 角色关联信息",返回 "用户名、部门名称、角色名称"。
新增角色表与关联表:
- 角色表(role):id、role_name(角色名称);
代码示例:
java
public List<Map<String, Object>> getUserDeptRole() {
MPJLambdaWrapper<User> wrapper = new MPJLambdaWrapper<>(User.class);
// 1. 左连接部门表
wrapper.joinLeft(Dept.class, Dept::getId, User::getDeptId)
// 2. 左连接用户角色关联表(user_role),关联条件:user.id = user_role.user_id
.joinLeft(UserRole.class, UserRole::getUserId, User::getId)
// 3. 左连接角色表(role),关联条件:user_role.role_id = role.id
.joinLeft(Role.class, Role::getId, UserRole::getRoleId)
// 4. 指定返回字段
.select(User::getUsername, Dept::getDeptName, Role::getRoleName);
return userMapper.selectList(wrapper);
}
4. 高级场景:自定义返回结果(非 Map 结构)
实际开发中,我们常需要将多表查询结果映射到自定义 DTO(数据传输对象),而非通用的 Map。此时可通过select方法的 Lambda 表达式指定 DTO 字段映射。
步骤:
- 定义自定义 DTO(如UserDeptDTO);
- 在select方法中通过DTO::字段指定映射关系。
代码示例:
java
// 1. 定义自定义DTO
@Data
public class UserDeptDTO {
private String username; // 用户名
private String deptName; // 部门名称
}
// 2. 多表查询映射到DTO
public List<UserDeptDTO> getUserDeptDTO() {
MPJLambdaWrapper<User> wrapper = new MPJLambdaWrapper<>(User.class);
wrapper.join(Dept.class, Dept::getId, User::getDeptId)
.eq(Dept::getStatus, 1)
// 映射DTO字段:User::getUsername → UserDeptDTO::getUsername
// Dept::getDeptName → UserDeptDTO::getDeptName
.select(
UserDeptDTO::getUsername, User::getUsername,
UserDeptDTO::getDeptName, Dept::getDeptName
);
// 注意:需使用MP提供的selectList方法,指定返回DTO类型
return userMapper.selectList(wrapper, UserDeptDTO.class);
}
四、常见问题与注意事项
1. 关联表字段冲突怎么办?
若多表存在同名字段(如create_time),需通过as方法给字段起别名,避免查询结果覆盖。
示例:
java
wrapper.join(Dept.class, Dept::getId, User::getDeptId)
// 给user的create_time起别名user_create_time
.select(User::getCreateTime, "user_create_time")
// 给dept的create_time起别名dept_create_time
.select(Dept::getCreateTime, "dept_create_time");
2. 如何支持复杂关联条件(如多字段关联)?
若关联条件需多个字段匹配(如user.dept_id = dept.id AND user.tenant_id = dept.tenant_id),可通过join方法的重载版本实现:
java
wrapper.join(Dept.class, deptWrapper -> {
// 多字段关联条件
deptWrapper.eq(Dept::getId, User::getDeptId)
.eq(Dept::getTenantId, User::getTenantId);
});
3. 为什么查询结果为 null?
常见原因:
- 关联条件错误(如外键字段匹配错误,User::getDeptId误写为User::getId);
- 表名与实体类@TableName注解不一致;
- 字段名与实体类@TableField注解不一致(若数据库字段为下划线命名,实体类需为驼峰命名,或通过@TableField指定)。
4. 是否支持子查询?
MPJLambdaWrapper 支持在where条件中嵌入子查询,通过inSql方法实现。例如:查询 "属于'技术部'的用户":
java
wrapper.join(Dept.class, Dept::getId, User::getDeptId)
.inSql(Dept::getId, "SELECT id FROM dept WHERE dept_name = '技术部'");
五、总结
MPJLambdaWrapper作为 MyBatis-Plus 的多表联查利器,完美解决了 "类型安全" 与 "灵活联查" 的平衡问题。核心要点可总结为:
- 用join/joinLeft指定关联表与关联条件;
- 用select指定返回字段(支持 Map 或自定义 DTO);
- 兼容 MP 原生的分页、排序、条件过滤等功能;
- 避免字段冲突时需用as起别名,关联条件错误需检查实体类注解。
掌握它后,开发者无需再手写复杂的多表联查 SQL,既能提升开发效率,又能减少硬编码带来的 bug。建议在实际项目中结合具体场景灵活使用,进一步挖掘 MP 的高效开发能力。