MyBatis面试高频:一对多关联查询,3种实现方式吃透

0 阅读9分钟

在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"&gt;

    <!-- 1. 主查询查询指定ID的部门信息 -->
    <select id="selectDeptWithEmpByStep" resultMap="deptEmpStepResultMap" parameterType="Integer">
        SELECT dept_id, dept_name, dept_address FROM t_department WHERE dept_id = #{deptId}
    &lt;/select&gt;

    <!-- 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}
    &lt;/select&gt;

    <!-- 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"/&gt;

        <!-- 配置一对多关联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"&gt;

    <!-- 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}
    &lt;/select&gt;

    <!-- 2. 结果映射:将联表查询结果映射到主实体和子实体集合 -->
    <resultMap id="deptEmpJoinResultMap" type="com.example.entity.Department"&gt;
        <!-- 主实体字段映射 -->
        <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"&gt;
            <!-- 子实体字段映射注意避免字段名冲突可通过别名区分-->
            <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问题关联逻辑简单、注解驱动开发

七、开发注意事项

  1. 主实体必须定义子实体的集合属性(如List employeeList),否则无法封装子数据;

  2. 关联字段必须对应:子实体的外键(deptId)需与主实体的主键(deptId)一致,否则无法关联数据;

  3. 嵌套结果(联表查询)需注意字段名冲突,建议通过别名(如e.dept_id AS emp_dept_id)区分主表和子表字段;

  4. 嵌套查询和注解方式存在N+1查询问题,若需优化,可通过配置延迟加载或改用联表查询;

  5. 注解方式中,@Many注解的select属性需填写子查询方法的全路径(包名+类名+方法名),否则会报错。

以上就是MyBatis一对多关联查询的三种核心实现方式,面试时只需抓住“嵌套查询vs联表查询vs注解方式”的核心差异,结合适用场景作答,就能轻松应对相关问题。实际开发中,可根据子数据量大小、性能需求和开发习惯,选择合适的实现方式,既能保证代码可读性,也能提升系统性能。