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

相关推荐
仍然.1 小时前
SpringCloud---Eureka介绍
java·spring cloud·eureka
霸道流氓气质1 小时前
Spring 事务同步机制 —— AfterTransactionActionCollector 的异步解耦原理
java·后端·spring
Lightpwd1 小时前
PostgreSQL jsonb 写入踩坑:一条 `\u0000` 如何让整批同步失败,还把异常伪装成“数据完整性冲突”
java
周周哈哈哈2 小时前
线程创建、执行、退出、回收
java·开发语言
云边有个稻草人2 小时前
飞算JavaAI能处理SaaS套餐变更业务吗?
java·springboot·ai编程·java开发·飞算javaai·java代码生成·ai coding模型
萧瑟余晖2 小时前
Java深入解析篇三十六之分布式系统详解
java·开发语言·分布式
KaKa_大王3 小时前
关于秒杀项目的一些理解
java·学习
tomla3 小时前
使用Java VisualVM观察过期对象引用现象
java
M1A13 小时前
Spring Boot YAML 配置读取完全指南:从基础到微服务
java