MyBatis一对多关系映射方式

1. MyBatis N+1 问题详解

1.1 什么是N+1问题

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

1.2 示例场景

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

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

xml 复制代码
<!-- 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>

执行过程:

java 复制代码
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 实体类定义

java 复制代码
// 部门实体
@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>标签指定好主键:

xml 复制代码
<!-- 使用 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 使用实例

java 复制代码
@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 复杂场景(多层嵌套)

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

java 复制代码
// 公司实体
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映射如下:

xml 复制代码
<!-- 更新后的结果映射,包含所有字段 -->
<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处理查询公司详情信息的方法:

java 复制代码
@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方案。

相关推荐
程序员清风7 小时前
滴滴二面:MySQL执行计划中,Key有值,还是很慢怎么办?
java·后端·面试
白鲸开源7 小时前
3.1.8<3.2.0<3.3.1,Apache DolphinScheduler集群升级避坑指南
java·开源·github
huohaiyu7 小时前
synchronized (Java)
java·开发语言·安全·synchronized
梵得儿SHI7 小时前
Java 工具类详解:Arrays、Collections、Objects 一篇通关
java·工具类·collections·arrays·objects
熊小猿7 小时前
Spring Boot 的 7 大核心优势
java·spring boot·后端
摸鱼的老谭7 小时前
Java学习之旅第二季-13:方法重写
java·学习·方法重写
云灬沙7 小时前
IDEA2025无法更新使用Terminal控制台
java·intellij-idea·idea·intellij idea
Yield & Allure7 小时前
IDEA在plugins里搜不到mybatisx插件的解决方法
java·ide·intellij-idea
yunmi_7 小时前
安全框架 SpringSecurity 入门(超详细,IDEA2024)
java·spring boot·spring·junit·maven·mybatis·spring security