MyBatis一对多关系映射方式小结

 更新时间:2025年10月11日 09:52:44   作者:Iris761  
在实际开发中经常会有一对多的表关系,因此掌握使用MyBatis处理一对多关系映射方式时很有必要的,本文就来介绍一下MyBatis一对多关系映射方式,感兴趣的可以了解一下

1. MyBatis N+1 问题详解

1.1 什么是N+1问题

N+1问题是指执行1次主查询获取N条主记录,然后对每条主记录再执行1次关联查询,总共执行 1 + N 次查询的性能问题。

1.2 示例场景

假设有:部门表(department)和员工表(employee),一个部门有多个员工。

产生N+1问题的代码如下:

<!-- 1. 先查询所有部门 -->
<select id="selectAllDepartments" resultMap="DepartmentResultMap">
    SELECT id, name FROM department
</select>

<!-- 2. 为每个部门查询员工 -->
<select id="selectEmployeesByDeptId" resultType="Employee">
    SELECT id, name FROM employee WHERE dept_id = #{deptId}
</select>

<!-- 3. 结果映射中使用嵌套查询 -->
<resultMap id="DepartmentResultMap" type="Department">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <collection property="employees" column="id" 
                ofType="Employee" select="selectEmployeesByDeptId"/>
</resultMap>

执行过程:

List<Department> departments = departmentMapper.selectAllDepartments();
// 实际执行的SQL:
// 1. SELECT id, name FROM department; (假设返回3个部门)
// 2. SELECT id, name FROM employee WHERE dept_id = 1;
// 3. SELECT id, name FROM employee WHERE dept_id = 2;
// 4. SELECT id, name FROM employee WHERE dept_id = 3;
// 总共执行了 1 + 3 = 4 次查询

1.3 产生问题

  1. 性能低下:查询次数随数据量线性增长
  2. 数据库压力大:频繁建立数据库连接
  3. 响应时间长:网络往返次数多

接下来讲解解决1+N问题的方式

2. 使用JOIN 查询 + Collection 映射

2.1 实体类定义

// 部门实体
@Getter
@Setter
public class Department {
    private Long id;
    private String name;
    private List<Employee> employees; // 一对多关系
}

// 员工实体
@Getter
@Setter
public class Employee {
    private Long id;
    private String name;
    private String position;
    private Long deptId;
}

2.2 XML配置

由于 JOIN 查询有些情况会产生重复的部门数据,MyBatis 会自动处理这种重复,但需要使用<id>标签指定好主键:

<!-- 使用 JOIN 查询一次性获取所有数据 -->
<resultMap id="DepartmentWithEmployeesMap" type="Department">
    <id property="id" column="dept_id"/><!-- 重要配置 -->
    <result property="name" column="dept_name"/>
    
    <!-- 使用 collection 映射一对多关系 -->
    <collection property="employees" ofType="Employee" javaType="java.util.ArrayList">
        <id property="id" column="emp_id"/><!-- 重要配置 -->
        <result property="name" column="emp_name"/>
        <result property="position" column="position"/>
        <result property="deptId" column="dept_id"/>
    </collection>
</resultMap>

<select id="selectDepartmentWithEmployees" resultMap="DepartmentWithEmployeesMap">
    SELECT 
        d.id as dept_id,
        d.name as dept_name,
        e.id as emp_id,
        e.name as emp_name,
        e.position,
        e.dept_id
    FROM department d
    LEFT JOIN employee e ON d.id = e.dept_id
    WHERE d.id = #{id}
</select>

<!-- 查询多个部门及其员工 -->
<select id="selectAllDepartmentsWithEmployees" resultMap="DepartmentWithEmployeesMap">
    SELECT 
        d.id as dept_id,
        d.name as dept_name,
        e.id as emp_id,
        e.name as emp_name,
        e.position,
        e.dept_id
    FROM department d
    LEFT JOIN employee e ON d.id = e.dept_id
    ORDER BY d.id, e.id
</select>

2.3 使用实例

@Service
public class DepartmentService {
    
    @Autowired
    private DepartmentMapper departmentMapper;
    
    // 一次性获取部门及其所有员工,避免N+1问题
    public Department getDepartmentWithEmployees(Long deptId) {
        return departmentMapper.selectDepartmentWithEmployees(deptId);
    }
    
    // 获取所有部门及其员工
    public List<Department> getAllDepartmentsWithEmployees() {
        return departmentMapper.selectAllDepartmentsWithEmployees();
    }
    
    // 业务方法:统计各部门员工数量
    public Map<String, Integer> getEmployeeCountByDepartment() {
        List<Department> departments = departmentMapper.selectAllDepartmentsWithEmployees();
        
        return departments.stream()
            .collect(Collectors.toMap(
                Department::getName,
                dept -> dept.getEmployees() != null ? dept.getEmployees().size() : 0
            ));
    }
}

2.4 复杂场景(多层嵌套)

假设有一个多层嵌套的复杂场景,表关系如下:

// 公司实体
public class Company {
    private Long id;
    private String name;
    private List<Department> departments; // 一对多:公司有多个部门
}

// 部门实体
public class Department {
    private Long id;
    private String name;
    private Long companyId;           // 所属公司ID
    private List<Employee> employees; // 一对多:部门有多个员工
    private List<Project> projects;   // 一对多:部门有多个项目
}

// 员工实体
public class Employee {
    private Long id;
    private String name;
    private String position;      // 新增字段
    private Long deptId;         // 所属部门ID
    private List<Skill> skills;  // 多对多:员工有多个技能
}

// 项目实体
public class Project {
    private Long id;
    private String name;
    private Long deptId;         // 所属部门ID
    private Date startDate;      // 新增字段
    private Date endDate;        // 新增字段
}

// 技能实体
public class Skill {
    private Long id;
    private String name;
    private String category;     // 新增字段:技能分类
}

// 员工技能关联实体(多对多中间表)
public class EmployeeSkill {
    private Long id;
    private Long employeeId;
    private Long skillId;
    private Integer proficiency; // 熟练程度
}

对应的Mapper映射如下:

<!-- 更新后的结果映射,包含所有字段 -->
<resultMap id="CompanyResultMap" type="Company">
    <id property="id" column="company_id"/>
    <result property="name" column="company_name"/>
    <collection property="departments" ofType="Department" resultMap="DepartmentResultMap"/>
</resultMap>

<resultMap id="DepartmentResultMap" type="Department">
    <id property="id" column="dept_id"/>
    <result property="name" column="dept_name"/>
    <result property="companyId" column="company_id"/>
    <collection property="employees" ofType="Employee" resultMap="EmployeeResultMap"/>
    <collection property="projects" ofType="Project" resultMap="ProjectResultMap"/>
</resultMap>

<resultMap id="EmployeeResultMap" type="Employee">
    <id property="id" column="emp_id"/>
    <result property="name" column="emp_name"/>
    <result property="position" column="position"/>
    <result property="deptId" column="dept_id"/>
    <collection property="skills" ofType="Skill" resultMap="SkillResultMap"/>
</resultMap>

<resultMap id="ProjectResultMap" type="Project">
    <id property="id" column="project_id"/>
    <result property="name" column="project_name"/>
    <result property="deptId" column="dept_id"/>
    <result property="startDate" column="start_date"/>
    <result property="endDate" column="end_date"/>
</resultMap>

<resultMap id="SkillResultMap" type="Skill">
    <id property="id" column="skill_id"/>
    <result property="name" column="skill_name"/>
    <result property="category" column="category"/>
</resultMap>

<!-- 更新后的查询SQL,包含所有字段 -->
<select id="selectCompanyWithDetails" resultMap="CompanyResultMap">
    SELECT 
        c.id as company_id, 
        c.name as company_name,
        d.id as dept_id, 
        d.name as dept_name,
        d.company_id,
        e.id as emp_id, 
        e.name as emp_name,
        e.position,
        e.dept_id,
        p.id as project_id, 
        p.name as project_name,
        p.dept_id,
        p.start_date,
        p.end_date,
        s.id as skill_id, 
        s.name as skill_name,
        s.category
    FROM company c
    LEFT JOIN department d ON c.id = d.company_id
    LEFT JOIN employee e ON d.id = e.dept_id
    LEFT JOIN project p ON d.id = p.dept_id
    LEFT JOIN employee_skill es ON e.id = es.employee_id
    LEFT JOIN skill s ON es.skill_id = s.id
    WHERE c.id = #{id}
</select>

3. 分次查询+Stream处理

还有一种方式是通过分次(次数为关联表的个数)查询关联表后,再使用Stream流组装数据,下面是通过分次查询+Stream处理查询公司详情信息的方法:

@Service
public class CompanyService {
    
    public Company getCompanyWithDetails(Long companyId) {
        // 1. 查询公司
        Company company = companyMapper.selectById(companyId);
        if (company == null) return null;
        
        // 2. 查询部门
        List<Department> departments = departmentMapper.selectByCompanyId(companyId);
        
        // 3. 查询员工(批量查询避免N+1)
        List<Long> deptIds = departments.stream()
            .map(Department::getId)
            .collect(Collectors.toList());
        List<Employee> employees = employeeMapper.selectByDeptIds(deptIds);
        
        // 4. 使用Stream组装数据
        Map<Long, List<Employee>> employeeMap = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDeptId));
        
        departments.forEach(dept -> 
            dept.setEmployees(employeeMap.getOrDefault(dept.getId(), new ArrayList<>()))
        );
        
        company.setDepartments(departments);
        return company;
    }
}

4. 性能对比

同样的业务下(查询公司详情信息)他们的性能对比表如下:

方面JOIN+Collection分次查询+Stream
数据库查询次数1次3次
网络开销中等
数据库压力单次复杂查询多次简单查询
内存占用可能有重复数据数据更紧凑
响应时间稳定但可能较长可能更快(并行查询)

总结:建议在管理后台数据展示这样的小数据量场景使用Join+Collection方案,在API接口大数据量这样较大数据量使用分次查询+Stream方案。

到此这篇关于MyBatis一对多关系映射方式小结的文章就介绍到这了,更多相关MyBatis一对多关系映射内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 一文掌握Java开发工具Maven(简单上手)

    一文掌握Java开发工具Maven(简单上手)

    掌握maven的相关知识是Java开发必备的技能,今天通过本文从入门安装开始,逐步深入讲解maven的相关知识,包括maven的安装到简单上手maven项目开发,感兴趣的朋友跟随小编一起看看吧
    2021-06-06
  • Java编程倒计时实现方法示例

    Java编程倒计时实现方法示例

    这篇文章主要介绍了Java编程倒计时实现的三个示例,三种实现方法,具有一定参考价值,需要的朋友可以了解下。
    2017-09-09
  • 全面解析Java中的GC与幽灵引用

    全面解析Java中的GC与幽灵引用

    一般的应用程序不会涉及到 Reference 编程, 但是了解这些知识会对理解 GC 的工作原理以及性能调优有一定帮助,在实现一些基础性设施比如缓存时也可能会用到,希望本文能有所帮助
    2013-09-09
  • 一篇文章带你学习JAVA MyBatis底层原理

    一篇文章带你学习JAVA MyBatis底层原理

    近来想写一个mybatis的分页插件,但是在写插件之前肯定要了解一下mybatis具体的工作原理吧,本文就详细总结了MyBatis工作原理,,需要的朋友可以参考下
    2021-09-09
  • Java集合TreeSet用法详解

    Java集合TreeSet用法详解

    本文详细讲解了Java集合TreeSet用法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-12-12
  • java内存模型jvm虚拟机简要分析

    java内存模型jvm虚拟机简要分析

    Java 内存模型的主要目的是定义程序中各种变量的访问规则, 关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节
    2021-09-09
  • servlet下载文件实现代码详解(五)

    servlet下载文件实现代码详解(五)

    这篇文章主要为大家详细介绍了servlet下载文件的实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-09-09
  • java的三种IO模型详解(BIO、NIO、AIO)

    java的三种IO模型详解(BIO、NIO、AIO)

    本文介绍了BIO、NIO和AIO三种不同的IO模型,分别分析了它们的工作机制、实现方式以及与BIO的对比,BIO是阻塞的,每个连接需要一个线程;NIO是同步非阻塞的,通过缓冲区和选择器实现I/O多路复用;AIO是异步的,操作系统处理IO操作,完成后通知应用程序
    2024-11-11
  • idea的终端(Terminal)cmd的命令换成linux的命令详解

    idea的终端(Terminal)cmd的命令换成linux的命令详解

    本文介绍IDEA配置Git的步骤:安装Git、修改终端设置并重启IDEA,强调顺序,作为个人经验分享,希望提供参考并支持脚本之家
    2025-07-07
  • 浅谈Maven resrouce下filtering作用

    浅谈Maven resrouce下filtering作用

    Filtering是Maven Resources Plugin的一个功能,本文主要介绍了浅谈Maven resrouce下filtering作用,具有一定的参考价值,感兴趣的可以了解一下
    2024-03-03

最新评论