在MyBatis开发中,一对多关联查询是日常开发和面试的高频考点,比如“一个用户对应多个订单”“一个部门对应多个员工”,这类场景都需要通过关联查询实现数据的联动获取。很多开发者在实现时容易混淆嵌套查询和嵌套结果的用法,也不清楚注解方式的配置要点,本文将聚焦一对多关联查询的核心实现方式,结合独立构思的代码示例,清晰拆解每种方式的实现步骤、适用场景及优缺点,帮你快速掌握考点,规避开发误区。
一、一句话吃透一对多关联查询核心
MyBatis实现一对多关联查询有三种核心方式:一是XML映射文件中使用标签实现嵌套查询(分步查询),二是使用标签实现嵌套结果(联表查询),三是通过@Results、@Result和@Many注解实现注解式关联,核心是在主实体类中定义子实体集合属性,通过关联字段建立主从关系映射。
二、核心前提:定义实体类(基础准备)
无论采用哪种实现方式,首先需定义主实体和子实体,且主实体中必须包含子实体的集合属性,用于存储关联的子数据。以下以“部门(Department)对应多个员工(Employee)”为场景,定义实体类,后续所有示例均基于此场景展开。
// 主实体:部门(一对多中的“一”)
public class Department {
private Integer deptId; // 部门ID(主键)
private String deptName; // 部门名称
private String deptAddress; // 部门地址
// 子实体集合:一个部门对应多个员工(一对多中的“多”)
private List<Employee> employeeList;
// getter、setter、toString方法(省略,实际开发需完整实现)
}
// 子实体:员工(一对多中的“多”)
public class Employee {
private Integer empId; // 员工ID(主键)
private String empName; // 员工姓名
private Integer empAge; // 员工年龄
private Integer deptId; // 关联字段:对应部门ID(外键)
// getter、setter、toString方法(省略,实际开发需完整实现)
}
注意:子实体中的关联字段(deptId)需与主实体的主键(deptId)对应,这是建立一对多关联的核心前提,确保能通过该字段关联主从表数据。
三、方式一:XML嵌套查询(分步查询)
核心原理
嵌套查询又称分步查询,核心逻辑是:先执行主查询,获取主实体(部门)数据,再根据主实体的主键(deptId),触发子查询,查询该部门对应的所有子实体(员工)数据,最终将子数据封装到主实体的集合属性中。
实现步骤(XML映射文件)
需编写主查询、子查询和结果映射,通过标签配置子查询关联,具体如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.DepartmentMapper">
<!-- 1. 主查询:查询指定ID的部门信息 -->
<select id="selectDeptWithEmpByStep" resultMap="deptEmpStepResultMap" parameterType="Integer">
SELECT dept_id, dept_name, dept_address FROM t_department WHERE dept_id = #{deptId}
</select>
<!-- 2. 子查询:根据部门ID,查询该部门下的所有员工 -->
<select id="selectEmpByDeptId" resultType="com.example.entity.Employee" parameterType="Integer">
SELECT emp_id, emp_name, emp_age, dept_id FROM t_employee WHERE dept_id = #{deptId}
</select>
<!-- 3. 结果映射:关联主实体和子实体集合 -->
<resultMap id="deptEmpStepResultMap" type="com.example.entity.Department">
<!-- 主实体字段映射 -->
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<result column="dept_address" property="deptAddress"/>
<!-- 配置一对多关联:collection标签对应主实体的集合属性 -->
<collection
property="employeeList" <!-- 主实体中存储子实体的集合属性名 -->
ofType="com.example.entity.Employee" <!-- 子实体的全类名 -->
select="selectEmpByDeptId" <!-- 子查询的ID -->
column="dept_id" <!-- 传递给子查询的关联字段(主实体主键) -->
/>
</resultMap>
</mapper>
调用示例(测试代码)
// 获取SqlSessionFactory(省略配置加载过程)
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
// 获取Mapper接口实例
DepartmentMapper deptMapper = sqlSession.getMapper(DepartmentMapper.class);
// 调用关联查询方法,查询ID为1的部门及下属员工
Department department = deptMapper.selectDeptWithEmpByStep(1);
System.out.println("部门信息:" + department);
System.out.println("该部门下属员工:" + department.getEmployeeList());
// 关闭会话
sqlSession.close();
优缺点与适用场景
优点:逻辑清晰,分步查询的职责明确,无需编写复杂的联表SQL,后期维护难度低;支持延迟加载(需额外配置),可按需加载子数据,减少不必要的查询。
缺点:会产生N+1查询问题——主查询执行1次,若主查询返回N个主实体,就会执行N次子查询,当子数据量较大时,会频繁访问数据库,影响性能。
适用场景:子数据量较小、需要延迟加载,或追求代码逻辑清晰的场景(如后台管理系统中,点击部门再加载下属员工)。
四、方式二:XML嵌套结果(联表查询)
核心原理
嵌套结果又称联表查询,核心逻辑是:通过一次SQL联表查询(LEFT JOIN),将主表(t_department)和子表(t_employee)的所有关联数据一次性查询出来,再通过标签配置结果映射,将子表数据封装到主实体的集合属性中,无需多次查询数据库。
实现步骤(XML映射文件)
只需编写一次联表SQL和对应的结果映射,无需拆分主查询和子查询,具体如下:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.mapper.DepartmentMapper">
<!-- 1. 联表查询:一次性获取部门和下属员工的所有数据 -->
<select id="selectDeptWithEmpByJoin" resultMap="deptEmpJoinResultMap" parameterType="Integer">
SELECT
d.dept_id, d.dept_name, d.dept_address,
e.emp_id, e.emp_name, e.emp_age, e.dept_id AS emp_dept_id
FROM t_department d
LEFT JOIN t_employee e ON d.dept_id = e.dept_id
WHERE d.dept_id = #{deptId}
</select>
<!-- 2. 结果映射:将联表查询结果映射到主实体和子实体集合 -->
<resultMap id="deptEmpJoinResultMap" type="com.example.entity.Department">
<!-- 主实体字段映射 -->
<id column="dept_id" property="deptId"/>
<result column="dept_name" property="deptName"/>
<result column="dept_address" property="deptAddress"/>
<!-- 配置一对多关联:嵌套结果映射子实体集合 -->
<collection property="employeeList" ofType="com.example.entity.Employee">
<!-- 子实体字段映射,注意避免字段名冲突(可通过别名区分) -->
<id column="emp_id" property="empId"/>
<result column="emp_name" property="empName"/>
<result column="emp_age" property="empAge"/>
<result column="emp_dept_id" property="deptId"/>
</collection>
</resultMap>
</mapper>
调用示例(测试代码)
// 与嵌套查询的调用方式完全一致,无需修改
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
DepartmentMapper deptMapper = sqlSession.getMapper(DepartmentMapper.class);
Department department = deptMapper.selectDeptWithEmpByJoin(1);
System.out.println("部门信息:" + department);
System.out.println("该部门下属员工:" + department.getEmployeeList());
sqlSession.close();
优缺点与适用场景
优点:仅执行一次SQL查询,减少数据库访问次数,性能更优;无需担心N+1查询问题,适合大数据量场景。
缺点:SQL语句复杂度较高,当主表和子表字段较多时,需手动处理字段别名,避免字段名冲突;无法实现延迟加载,每次都会一次性加载所有关联数据。
适用场景:需要一次性加载所有关联数据,子数据量较大,且追求查询性能的场景(如报表统计、数据导出)。
五、方式三:注解方式实现(注解驱动)
核心原理
对于不喜欢XML配置的开发者,MyBatis支持通过注解方式实现一对多关联查询,核心是通过@Select注解编写查询SQL,@Results和@Result注解配置字段映射,@Many注解关联子查询方法,实现逻辑与XML嵌套查询一致(分步查询)。
实现步骤(Mapper接口)
无需编写XML映射文件,直接在Mapper接口中通过注解配置,具体如下:
package com.example.mapper;
import com.example.entity.Department;
import com.example.entity.Employee;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import java.util.List;
public interface DepartmentMapper {
// 主查询:查询指定ID的部门信息,通过注解配置关联关系
@Select("SELECT dept_id, dept_name, dept_address FROM t_department WHERE dept_id = #{deptId}")
@Results({
// 主实体字段映射
@Result(column = "dept_id", property = "deptId", id = true),
@Result(column = "dept_name", property = "deptName"),
@Result(column = "dept_address", property = "deptAddress"),
// 配置一对多关联:@Many对应子查询方法
@Result(
property = "employeeList", // 主实体的集合属性名
column = "dept_id", // 传递给子查询的关联字段
many = @Many(select = "com.example.mapper.DepartmentMapper.selectEmpByDeptId") // 子查询方法全路径
)
})
Department selectDeptWithEmpByAnnotation(Integer deptId);
// 子查询:根据部门ID查询下属员工(与XML嵌套查询的子查询逻辑一致)
@Select("SELECT emp_id, emp_name, emp_age, dept_id FROM t_employee WHERE dept_id = #{deptId}")
List<Employee> selectEmpByDeptId(Integer deptId);
}
调用示例(测试代码)
与XML方式的调用完全一致,无需修改任何代码,直接调用Mapper接口方法即可:
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sqlSessionFactory.openSession();
DepartmentMapper deptMapper = sqlSession.getMapper(DepartmentMapper.class);
Department department = deptMapper.selectDeptWithEmpByAnnotation(1);
System.out.println("部门信息:" + department);
System.out.println("该部门下属员工:" + department.getEmployeeList());
sqlSession.close();
优缺点与适用场景
优点:无需编写XML映射文件,代码简洁,开发效率高;适合注解驱动开发的项目,降低配置复杂度。
缺点:复杂关联场景(如多表联查、字段映射复杂)下,注解配置会显得繁琐,可读性差;同样存在N+1查询问题。
适用场景:关联逻辑简单、追求开发效率,且采用注解驱动开发的项目。
六、三种方式核心对比总结表
| 实现方式 | 查询次数 | 核心配置 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|---|
| XML嵌套查询(分步) | 1+N次 | XML中+select属性 | 逻辑清晰,支持延迟加载,维护简单 | 存在N+1查询问题,性能一般 | 子数据量小、需延迟加载 |
| XML嵌套结果(联表) | 1次 | XML中+联表SQL | 性能优,无N+1问题 | SQL复杂,需处理字段别名,不支持延迟加载 | 子数据量大、需一次性加载 |
| 注解方式 | 1+N次 | @Results+@Many注解 | 代码简洁,开发效率高 | 复杂关联可读性差,存在N+1问题 | 关联逻辑简单、注解驱动开发 |
七、开发注意事项
-
主实体必须定义子实体的集合属性(如List employeeList),否则无法封装子数据;
-
关联字段必须对应:子实体的外键(deptId)需与主实体的主键(deptId)一致,否则无法关联数据;
-
嵌套结果(联表查询)需注意字段名冲突,建议通过别名(如e.dept_id AS emp_dept_id)区分主表和子表字段;
-
嵌套查询和注解方式存在N+1查询问题,若需优化,可通过配置延迟加载或改用联表查询;
-
注解方式中,@Many注解的select属性需填写子查询方法的全路径(包名+类名+方法名),否则会报错。
以上就是MyBatis一对多关联查询的三种核心实现方式,面试时只需抓住“嵌套查询vs联表查询vs注解方式”的核心差异,结合适用场景作答,就能轻松应对相关问题。实际开发中,可根据子数据量大小、性能需求和开发习惯,选择合适的实现方式,既能保证代码可读性,也能提升系统性能。