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> 的两种配置方式(嵌套结果与嵌套查询),并合理运用延迟加载机制,能够让你在面对复杂关联查询时游刃有余。

相关推荐
EatFan8 小时前
Spring Boot 4 落地观察:从 yudao-cloud、matecloud、JPower 看国产脚手架的升级路线与迁移清单
java·spring boot·后端·spring cloud·微服务·后端开发·jdk 21
Maiko Star8 小时前
* LangChain 提示词模板详解:ChatPromptTemplate 的使用与高级特性
java·人工智能·langchain
现任明教教主~10 小时前
Thinkphp站群蜘蛛池SaaS系统YanyvSEO含多用户/积分/六大引擎计费/易支付对接
java·开发语言·spring
一条破秋裤11 小时前
Linux 线程创建:pthread_create 与基本回收
java·linux·运维
布吉岛的石头11 小时前
Java 程序员第 49 阶段5:BERT 预训练目标 MLM+NSP 的工程含义
java·人工智能·深度学习·bert·transformer
摇滚侠12 小时前
《On Java 中文版 基础卷》阅读笔记 对象无处不在 03
java·笔记·python
二十雨辰12 小时前
[学成在线]-01项目背景
java
弈栈录13 小时前
Java AI 应用的异步化与高并发设计
java·后端·架构
落魄实习生13 小时前
Agent Scope Java 2.x 系列【2】 ReActAgent
java·开发语言
艸肅13 小时前
Ubuntu 安装 JDK (含手动安装)
java·ubuntu·jdk