第 4 章 [扩展性] 复杂关系映射与性能优化
第 4 章 [扩展性] 复杂关系映射与性能优化
Prorise第 4 章 [扩展性] 复杂关系映射与性能优化
摘要: 现实世界的业务远不止单表操作那么简单。本章我们将为银行应用引入“客户”实体,并深入探讨如何处理 “一个客户拥有多个账户” (一对多) 以及 “多个账户属于一个客户” (多对一) 这样的关联关系。您将掌握 MyBatis 中最强大的结果映射工具 <resultMap>,以及用于处理关联关系的 <association> 和 <collection> 标签。在此基础上,我们将进一步探讨性能优化这一高级主题,揭示著名的 N+1 查询问题,并学习如何通过 延迟加载 和 一、二级缓存 来显著提升应用性能。
4.1. [基础] resultMap:复杂结果映射的基石
到目前为止,我们一直使用 resultType 来自动映射查询结果,这在列名与属性名(或开启驼峰映射后)能对应上的情况下非常方便。但当遇到多表查询、列名与属性名无规律对应时,我们就需要一个更强大的工具——<resultMap>。
<resultMap> 允许我们手动定义数据库查询结果(ResultSet)的列与 Java 对象(POJO)的属性之间的映射关系。
核心标签与属性讲解:
<resultMap>: 定义一个结果映射集。id: 必须。为此<resultMap>指定一个唯一的名称,供<select>标签引用。type: 必须。指定要映射到的 POJO 类的全限定名或别名。
<id>: 用于映射主键字段,有助于 MyBatis 提高性能。property: POJO 中的属性名。column: 数据库结果集中的列名。
<result>: 用于映射普通字段。property: POJO 中的属性名。column: 数据库结果集中的列名。
需求: 假设 t_car 表的列名不规范(例如 car_number, car_brand),我们需要手动将其映射到 Car 对象的 carNum, brand 属性上。
文件路径 (XML): src/main/resources/mappers/CarMapper.xml
1 | <resultMap id="carResultMap" type="com.example.bank.pojo.Car"> |
通过这种方式,无论列名多么不规范,我们都能精确地将其映射到正确的 Java 属性上。
4.2. [核心] 处理关联关系
现在,我们来升级我们的银行应用业务模型。
1. 场景与准备工作
一个客户(Customer)可以拥有多个账户(Account)。这是一个典型的“一对多”关系。反过来,一个账户(Account)只属于一个客户(Customer),这是一个“多对一”关系。
- 数据库准备:
我们新建t_customer表,并为t_account表添加外键customer_id。
1 | -- 新建客户表 |
- POJO 准备:
文件路径:src/main/java/com/example/bank/pojo/Customer.java(新建)
1 | package com.example.bank.pojo; |
文件路径: src/main/java/com/example/bank/pojo/Account.java (修改)
1 | package com.example.bank.pojo; |
2. 一对一 (<association>)
需求: 查询账户信息时,同时把所属的客户信息也查询出来。
我们采用“分步查询”的方式,这更灵活,且是实现后续“延迟加载”优化的前提。
association标签讲解:<association>用于在resultMap中处理“拥有一个”(has-one)的关联关系。property: POJO 中关联对象的属性名,例如customer。select: 指定一个外部查询的statementId(namespace.id),MyBatis 会执行这个查询来加载关联对象。column: 将主查询结果的某一列的值,作为参数传递给select指定的查询。
文件路径 (接口): src/main/java/com/example/bank/mapper/CustomerMapper.java (新建)
1 | package com.example.bank.mapper; |
文件路径 (XML): src/main/resources/mappers/CustomerMapper.xml (新建)
1 |
|
文件路径 (XML): src/main/resources/mappers/AccountMapper.xml (修改)
1 |
|
文件路径 (接口): src/main/java/com/example/bank/mapper/AccountMapper.java (添加方法)
1 | Account selectByActnoWithCustomer(String actno); |
编写测试
文件路径: src/test/java/com/example/bank/test/AssociationTest.java (新建)
1 |
|
3. 一对多 (<collection>)
需求: 查询客户信息时,同时把他名下所有的账户列表也查询出来。
collection标签讲解:<collection>用于处理“拥有多个”(has-many)的关联关系。property: POJO 中集合属性的名称,例如accounts。ofType: 集合的泛型类型,例如com.example.bank.pojo.Account。select和column的作用与<association>相同。
文件路径 (接口): src/main/java/com/example/bank/mapper/AccountMapper.java (添加方法)
1 | List<Account> selectByCustomerId(Integer customerId); |
文件路径 (XML): src/main/resources/mappers/AccountMapper.xml (添加)
1 | <select id="selectByCustomerId" resultType="com.example.bank.pojo.Account"> |
文件路径 (XML): src/main/resources/mappers/CustomerMapper.xml (修改)
1 | <resultMap id="customerWithAccountsMap" type="com.example.bank.pojo.Customer"> |
文件路径 (接口): src/main/java/com/example/bank/mapper/CustomerMapper.java (修改)
1 | Customer selectByIdWithAccounts(Integer id); |
编写测试
文件路径: src/test/java/com/example/bank/test/CollectionTest.java (新建)
1 | package com.example.bank.test; |
4.3. [性能] 性能优化
分步查询虽然灵活,但可能会引发著名的 N+1 查询问题:查询 1 个主对象(例如 Customer),触发了 N 个查询关联对象(Account)的 SQL。如果我们要查询 100 个客户,就会执行 1 + 100 = 101 条 SQL,效率低下。
1. 延迟加载
延迟加载是解决 N+1 问题的有效手段。它的核心思想是:非必要,不加载。只有当你真正去访问关联对象时(例如调用 customer.getAccounts()),MyBatis 才会去执行加载关联对象的 SQL。
- 如何开启:
在全局配置文件中开启即可。
文件路径: src/main/resources/mybatis-config.xml
1 |
|
- 演示:
文件路径:src/test/java/com/example/bank/test/PerformanceTest.java(新建)
2. MyBatis 缓存
缓存是另一种重要的性能优化手段,它能减少与数据库的交互次数。
一级缓存 (SqlSession 级别):
- 原理: 默认开启,与
SqlSession绑定。在同一个SqlSession的生命周期内,执行完全相同的查询(相同的statementId和参数),只有第一次会访问数据库,后续的都将直接从内存缓存中获取。 - 演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void testL1Cache() {
try (SqlSession sqlSession = SqlSessionUtil.openSession()) {
CustomerMapper mapper = sqlSession.getMapper(CustomerMapper.class);
log.info("--- 第一次查询 ---");
mapper.selectById(1);
log.info("--- 第二次查询,应命中一级缓存,不会再发 SQL ---");
mapper.selectById(1);
}
}
// 输出:
// ... --- 第一次查询 ---
// ... ==> Preparing: select * from t_customer where id = ?
// ... --- 第二次查询,应命中一级缓存,不会再发 SQL ---
// (此处无新的 SQL 日志)- 原理: 默认开启,与
二级缓存 (SqlSessionFactory 级别):
危险: 二级缓存是跨会话共享的,数据一致性较难保证,不同会话可能获取到旧数据。而且缓存过多数据会占用内存,不适合数据变化频繁的场景,所以默认不开启,让开发者根据实际需求灵活配置
✅ 适合开启二级缓存的场景
查询多、更新少(典型的报表系统、字典表、配置类数据)
单体应用或已配置了共享缓存方案(如整合 Redis)
严格管理 Mapper 操作范围,不跨 Mapper 操作同一张表
原理: 跨
SqlSession共享的缓存,需要手动开启。当一个SqlSession关闭或提交时,它的一级缓存中的数据会被刷新到二级缓存中。开启步骤:
- 在
mybatis-config.xml中确保cacheEnabled为true(默认就是)。 - 在需要开启二级缓存的 Mapper XML 文件中添加
<cache/>标签。 - 所有参与缓存的 POJO 必须实现
java.io.Serializable接口(我们之前已完成)。
文件路径:
src/main/resources/mappers/CustomerMapper.xml(添加)1
2
3
4<mapper namespace="com.example.bank.mapper.CustomerMapper">
<cache/>
...
</mapper>- 在







