MyBatis 多对一关联映射详解

MyBatis 多对一关联映射详解

一、概述

多对一(Many-to-One)是数据库设计中常见的关联关系。例如:多个员工(Employee)属于同一个部门(Department) 。在 MyBatis 中,多对一映射通常使用 <association> 标签来实现,其配置方式与一对一(One-to-One)几乎完全相同。

多对一与一对一的区别在于业务语义

  • 一对一:一个员工对应一个身份证(双向唯一)。
  • 多对一:多个员工对应同一个部门(从"多"方看向"一"方)。

二、场景与实体类

2.1 数据库表结构

sql 复制代码
-- 部门表(一方)
CREATE TABLE t_department (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    dept_name VARCHAR(100),
    location VARCHAR(100)
);

-- 员工表(多方)
CREATE TABLE t_employee (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    emp_name VARCHAR(100),
    salary DECIMAL(10,2),
    dept_id BIGINT,  -- 外键,指向 t_department.id
    FOREIGN KEY (dept_id) REFERENCES t_department(id)
);

2.2 Java 实体类

java 复制代码
// 部门实体(一方)
public class Department {
    private Long id;
    private String deptName;
    private String location;
    // getter/setter
}

// 员工实体(多方)
public class Employee {
    private Long id;
    private String empName;
    private Double salary;
    private Department department;  // 多对一:多个员工属于同一个部门
    // getter/setter
}

三、两种映射方式

3.1 方式一:嵌套结果(JOIN 查询,推荐)

使用一次 JOIN 查询,一次性获取员工和部门的所有字段,性能最优

XML 配置
xml 复制代码
<resultMap id="EmployeeDeptMap" type="Employee">
    <!-- 员工字段映射 -->
    <id property="id" column="e_id"/>
    <result property="empName" column="e_name"/>
    <result property="salary" column="salary"/>
    
    <!-- 多对一:每个员工关联一个部门 -->
    <association property="department" javaType="Department">
        <id property="id" column="d_id"/>
        <result property="deptName" column="dept_name"/>
        <result property="location" column="location"/>
    </association>
</resultMap>

<select id="selectEmployeeWithDept" resultMap="EmployeeDeptMap">
    SELECT 
        e.id AS e_id,
        e.emp_name AS e_name,
        e.salary,
        d.id AS d_id,
        d.dept_name,
        d.location
    FROM t_employee e
    LEFT JOIN t_department d ON e.dept_id = d.id
    WHERE e.id = #{id}
</select>
生成的 SQL
sql 复制代码
SELECT e.id AS e_id, e.emp_name AS e_name, e.salary,
       d.id AS d_id, d.dept_name, d.location
FROM t_employee e
LEFT JOIN t_department d ON e.dept_id = d.id
WHERE e.id = ?
优点
  • 一次查询,性能最高。
  • 数据完整性好,没有 N+1 问题。

3.2 方式二:嵌套查询(分步查询)

先查员工,再根据 dept_id 单独查部门。可以配合延迟加载实现按需查询。

XML 配置
xml 复制代码
<resultMap id="EmployeeDeptLazyMap" type="Employee">
    <id property="id" column="id"/>
    <result property="empName" column="emp_name"/>
    <result property="salary" column="salary"/>
    
    <!-- 多对一:通过另一个查询获取部门信息 -->
    <association property="department" 
                 javaType="Department"
                 select="com.xie.mapper.DepartmentMapper.selectById"
                 column="dept_id"
                 fetchType="lazy"/>  <!-- 延迟加载 -->
</resultMap>

<select id="selectEmployeeLazy" resultMap="EmployeeDeptLazyMap">
    SELECT id, emp_name, salary, dept_id
    FROM t_employee
    WHERE id = #{id}
</select>

对应的 DepartmentMapper

xml 复制代码
<select id="selectById" resultType="Department">
    SELECT * FROM t_department WHERE id = #{id}
</select>
优点
  • SQL 职责清晰,复用性强(selectById 可单独使用)。
  • 配合 fetchType="lazy",可实现按需加载。
缺点
  • 存在 N+1 查询风险(查询多个员工时,会额外执行多次部门查询)。

四、延迟加载配置

4.1 全局配置(mybatis-config.xml

xml 复制代码
<settings>
    <!-- 开启延迟加载 -->
    <setting name="lazyLoadingEnabled" value="true"/>
    <!-- 关闭激进加载,只在实际访问时触发 -->
    <setting name="aggressiveLazyLoading" value="false"/>
</settings>

4.2 局部配置(覆盖全局)

<association> 中通过 fetchType 精细化控制:

xml 复制代码
<!-- 立即加载(即使开启全局延迟,此关联也会立即加载) -->
<association property="department" fetchType="eager" .../>

<!-- 延迟加载(覆盖全局设置) -->
<association property="department" fetchType="lazy" .../>

五、级联属性操作(Cascade)

在多对一关系中,级联属性通常指:当操作(插入/更新)员工时,如何处理其关联的部门对象

5.1 查询时的级联属性

场景:查询员工时,需要同时返回部门信息(上文已实现)。

5.2 插入时的级联属性

场景:插入员工时,需要为员工关联一个已存在的部门。

Mapper 接口
java 复制代码
public interface EmployeeMapper {
    int insertEmployee(Employee employee);
}
XML 配置
xml 复制代码
<insert id="insertEmployee" useGeneratedKeys="true" keyProperty="id">
    INSERT INTO t_employee (emp_name, salary, dept_id)
    VALUES (#{empName}, #{salary}, #{department.id})
    <!-- 注意:通过 #{department.id} 获取关联部门的 ID -->
</insert>
调用代码
java 复制代码
// 先查询或创建一个部门
Department dept = departmentMapper.selectById(1L);
// 或 Department dept = new Department(); dept.setId(2L);

Employee emp = new Employee();
emp.setEmpName("李四");
emp.setSalary(8000.0);
emp.setDepartment(dept);  // 设置关联部门

employeeMapper.insertEmployee(emp);
sqlSession.commit();

生成的 SQL

sql 复制代码
INSERT INTO t_employee (emp_name, salary, dept_id) VALUES (?, ?, ?)
-- 参数:李四, 8000.0, 1

5.3 更新时的级联属性

场景:更新员工所属部门(更换部门)。

xml 复制代码
<update id="updateEmployeeDept">
    UPDATE t_employee
    SET dept_id = #{department.id}
    WHERE id = #{id}
</update>
调用代码
java 复制代码
Employee emp = employeeMapper.selectById(1L);
// 更换部门
Department newDept = departmentMapper.selectById(2L);
emp.setDepartment(newDept);
employeeMapper.updateEmployeeDept(emp);
sqlSession.commit();

六、多对一 vs 一对多 对比

对比维度 多对一 一对多
视角 从"多"方看向"一"方 从"一"方看向"多"方
实体类属性 private Department department; private List<Employee> employees;
XML 标签 <association> <collection>
标签属性 javaType="Department" ofType="Employee"
典型场景 员工 → 部门 部门 → 员工列表
外键所在表 在"多"方表中(t_employee.dept_id 在"多"方表中(t_employee.dept_id

七、常见错误与避坑指南

7.1 ❌ 忘记配置 <id> 导致数据重复

xml 复制代码
<association property="department" javaType="Department">
    <!-- ❌ 缺少 id,MyBatis 无法识别同一部门 -->
    <result property="deptName" column="dept_name"/>
</association>

<!-- ✅ 必须配置 id -->
<association property="department" javaType="Department">
    <id property="id" column="d_id"/>
    <result property="deptName" column="dept_name"/>
</association>

7.2 ❌ javaType 写错

xml 复制代码
<!-- ❌ 错误:department 是 Department 类型,不是 List -->
<association property="department" javaType="List">

<!-- ✅ 正确 -->
<association property="department" javaType="Department">

7.3 ❌ 列名冲突(多表 JOIN 时)

sql 复制代码
-- ❌ 错误:两表都有 id,覆盖
SELECT e.*, d.* FROM t_employee e LEFT JOIN t_department d ON ...

-- ✅ 正确:使用别名区分
SELECT e.id AS e_id, d.id AS d_id, ...

八、最佳实践总结

要点 建议
映射方式 优先使用嵌套结果(JOIN),一次查询,性能最优
延迟加载 仅当关联数据访问频率低时使用,需开启全局配置
关联字段 外键字段(如 dept_id)需在数据库中正确建立
插入/更新 通过 #{department.id} 获取关联对象的 ID
N+1 问题 使用嵌套查询时,注意控制数据量,避免循环查询

结语

多对一映射是 MyBatis 中最常见的关联关系之一,其本质与一对一相同,只是业务语义上代表了"多个子对象属于一个父对象"。掌握 <association> 的两种配置方式(嵌套结果与嵌套查询),并合理运用延迟加载机制,能够让你在面对复杂关联查询时游刃有余。

相关推荐
lhldsg2 小时前
全民健身解决方案小程序开发:从0到1的技术实战
java·数据库·需求分析
jason成都2 小时前
Spring WebFlux 适配达梦新方案|dm‑r2dbc:Netty 异步传输的实验性 R2DBC 驱动
java·后端·spring
泡海椒2 小时前
内置SPI函数库详解:JQuick-Java Builtin工具类实战用法
java·开发语言·python
辰烨chenye2 小时前
LeetCode Hot 100 题解 · 二分篇
java·算法·leetcode
小羊没烦恼!3 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
小荷才露尖尖角,早有蜻蜓立上头4 小时前
过滤器的4种创建方式
java
大圣编蚕4 小时前
Java ByteArrayInputStream 详解:从入门到实战
java·开发语言·算法
程序员阿明4 小时前
idea把插件啥的不默认放在C盘
java·ide·intellij-idea
linweidong5 小时前
中科闻歌Java面试题及参考答案
java·python·大模型·提示词·ai agent·mcp·rag原理
OpenAnolis小助手5 小时前
一句话看透 JVM,SysOM 诊断 Skill 新增 Java 应用诊断能力
java·开发语言·jvm·阿里云·操作系统控制台·sysom