MyBatis 中 resultMap、association、collection标签详解
MyBatis 中 resultMap、association、collection标签详解
- MyBatis 中 `<resultMap>`、`<association>` 与 `<collection>` 标签详解
- 🧱 前提:假设的业务模型
- 数据库表结构(简化版)
- 对应的 Java 实体类(简化)
- 1️⃣ `<resultMap>`:结果映射的根标签
- 作用
- 示例:基础 resultMap(无关联)
- 2️⃣ `<association>`:处理一对一关联
- 作用
- SQL 查询(连接查询)
- 对应的 `<resultMap>`
- 3️⃣ `<collection>`:处理一对多关联
- 作用
- SQL 查询(连接查询)
- 对应的 `<resultMap>`
- 🔁 补充:嵌套查询(N+1 问题)
- ✅ 总结对比
- 🔍 什么是 N+1 问题?
- 🧩 举个例子:订单与用户(一对一)
- ❌ 使用嵌套查询(引发 N+1)
- 执行过程:
- ⚠️ N+1 问题的危害
- ✅ 如何避免 N+1 问题?
- ✅ 推荐方案:使用 **JOIN 查询 + `<association>` / `<collection>`**
- 🆚 嵌套查询 vs JOIN 查询 对比
- 🔒 补充:MyBatis 的懒加载(Lazy Loading)
- ✅ 总结
MyBatis 中 <resultMap>、<association> 与 <collection> 标签详解
在 MyBatis 中,<resultMap>、<association> 和 <collection> 是处理复杂对象映射(尤其是对象之间存在一对一、一对多关系)时非常关键的标签。本文将逐一解释每个标签的作用,并配合具体的 SQL 示例和 Java 实体类结构,帮助你深入理解。
🧱 前提:假设的业务模型
我们以一个常见的“订单-用户-订单项”模型为例:
- User(用户):一对一关联订单(一个用户可以有多个订单,但这里我们先演示一对一)
- Order(订单):属于一个用户(一对一),包含多个订单项(一对多)
- OrderItem(订单项):属于一个订单
数据库表结构(简化版)
-- 用户表
CREATE TABLE user (id INT PRIMARY KEY,name VARCHAR(50)
);-- 订单表
CREATE TABLE orders (id INT PRIMARY KEY,user_id INT,order_no VARCHAR(50)
);-- 订单项表
CREATE TABLE order_item (id INT PRIMARY KEY,order_id INT,product_name VARCHAR(100),quantity INT
);
对应的 Java 实体类(简化)
public class User {private Integer id;private String name;// getter/setter
}public class Order {private Integer id;private String orderNo;private User user; // 一对一:订单属于一个用户private List<OrderItem> items; // 一对多:订单包含多个订单项// getter/setter
}public class OrderItem {private Integer id;private String productName;private Integer quantity;// getter/setter
}
1️⃣ <resultMap>:结果映射的根标签
作用
定义如何将数据库查询结果(ResultSet)映射到 Java 对象。可以包含普通字段映射、<association>(一对一)、<collection>(一对多)等。
示例:基础 resultMap(无关联)
<resultMap id="BaseOrderMap" type="Order"><id property="id" column="id"/><result property="orderNo" column="order_no"/>
</resultMap>
这个只是映射 Order 自身的字段,不涉及关联对象。
2️⃣ <association>:处理一对一关联
作用
用于映射单个对象属性(如 Order 中的 User)。
SQL 查询(连接查询)
SELECT o.id AS order_id,o.order_no,u.id AS user_id,u.name AS user_name
FROM orders o
JOIN user u ON o.user_id = u.id
WHERE o.id = #{orderId}
对应的 <resultMap>
<resultMap id="OrderWithUserMap" type="Order"><id property="id" column="order_id"/><result property="orderNo" column="order_no"/><!-- 一对一:关联 User 对象 --><association property="user" javaType="User"><id property="id" column="user_id"/><result property="name" column="user_name"/></association>
</resultMap><select id="getOrderWithUser" resultMap="OrderWithUserMap">SELECT o.id AS order_id,o.order_no,u.id AS user_id,u.name AS user_nameFROM orders oJOIN user u ON o.user_id = u.idWHERE o.id = #{orderId}
</select>
✅ 这样 MyBatis 就会自动将 user_id 和 user_name 映射到 Order.user 属性中。
3️⃣ <collection>:处理一对多关联
作用
用于映射集合属性(如 Order 中的 List<OrderItem>)。
SQL 查询(连接查询)
SELECT o.id AS order_id,o.order_no,u.id AS user_id,u.name AS user_name,oi.id AS item_id,oi.product_name,oi.quantity
FROM orders o
JOIN user u ON o.user_id = u.id
LEFT JOIN order_item oi ON o.id = oi.order_id
WHERE o.id = #{orderId}
注意:这里使用
LEFT JOIN,因为订单可能没有订单项。
对应的 <resultMap>
<resultMap id="OrderWithUserAndItemsMap" type="Order"><id property="id" column="order_id"/><result property="orderNo" column="order_no"/><!-- 一对一:User --><association property="user" javaType="User"><id property="id" column="user_id"/><result property="name" column="user_name"/></association><!-- 一对多:OrderItem 列表 --><collection property="items" ofType="OrderItem"><id property="id" column="item_id"/><result property="productName" column="product_name"/><result property="quantity" column="quantity"/></collection>
</resultMap><select id="getOrderWithDetails" resultMap="OrderWithUserAndItemsMap">SELECT o.id AS order_id,o.order_no,u.id AS user_id,u.name AS user_name,oi.id AS item_id,oi.product_name,oi.quantityFROM orders oJOIN user u ON o.user_id = u.idLEFT JOIN order_item oi ON o.id = oi.order_idWHERE o.id = #{orderId}
</select>
✅ MyBatis 会自动将多个 item_id 行聚合成一个 List<OrderItem>,并赋值给 Order.items。
⚠️ 注意:使用
<collection>时,必须确保主对象(Order)的主键(如order_id)在结果集中,MyBatis 才能正确分组聚合子对象。
🔁 补充:嵌套查询(N+1 问题)
除了上面的“连接查询”方式,MyBatis 也支持通过嵌套 select 实现关联(但可能引发 N+1 查询问题):
<resultMap id="OrderWithUserNested" type="Order"><id property="id" column="id"/><result property="orderNo" column="order_no"/><association property="user" column="user_id" select="com.example.mapper.UserMapper.selectUserById"/>
</resultMap><select id="selectUserById" resultType="User">SELECT * FROM user WHERE id = #{id}
</select>
这种方式虽然清晰,但每查一个订单就要再查一次用户,效率较低。推荐优先使用连接查询 + <association>/<collection> 的方式。
✅ 总结对比
| 标签 | 用途 | 对应关系 | 示例属性 |
|---|---|---|---|
<resultMap> | 定义结果映射规则 | — | id, type |
<association> | 映射单个对象 | 一对一 | property, javaType |
<collection> | 映射对象集合 | 一对多 | property, ofType |
当然可以!下面是对 MyBatis 嵌套查询中的 N+1 问题 的详细解释,包括其成因、影响、示例以及解决方案。
🔍 什么是 N+1 问题?
N+1 问题 是指在执行数据库查询时,先执行 1 次主查询,然后对每一条结果再执行 1 次子查询,总共执行 1 + N 次 SQL(N 是主查询返回的记录数)。
这在 ORM(如 MyBatis、Hibernate)中处理关联关系时非常常见,尤其是在使用 嵌套查询(Nested Select) 方式映射一对一或一对多关系时。
🧩 举个例子:订单与用户(一对一)
假设我们要查询 所有订单及其对应的用户信息。
❌ 使用嵌套查询(引发 N+1)
MyBatis 映射如下:
<resultMap id="OrderWithUserNested" type="Order"><id property="id" column="id"/><result property="orderNo" column="order_no"/><!-- 嵌套查询:每查一个订单,就调用一次 selectUserById --><association property="user"column="user_id"select="com.example.mapper.UserMapper.selectUserById"/>
</resultMap><select id="selectAllOrders" resultMap="OrderWithUserNested">SELECT id, order_no, user_id FROM orders
</select><select id="selectUserById" resultType="User">SELECT id, name FROM user WHERE id = #{id}
</select>
执行过程:
-
第 1 次查询(主查询):
SELECT id, order_no, user_id FROM orders;假设返回了 100 条订单记录(N = 100)。
-
接下来执行 100 次子查询(每条订单查一次用户):
SELECT id, name FROM user WHERE id = 1; SELECT id, name FROM user WHERE id = 2; ... SELECT id, name FROM user WHERE id = 100;
✅ 总共执行了 1 + 100 = 101 次 SQL 查询!
这就是典型的 N+1 问题。
⚠️ N+1 问题的危害
- 性能严重下降:大量数据库往返通信(round-trip),增加延迟。
- 数据库压力大:短时间内执行大量相似 SQL,可能拖垮数据库。
- 网络开销高:尤其在分布式系统中,影响更明显。
✅ 如何避免 N+1 问题?
✅ 推荐方案:使用 JOIN 查询 + <association> / <collection>
将关联数据通过 一次 JOIN 查询 获取,MyBatis 自动映射。
<resultMap id="OrderWithUserMap" type="Order"><id property="id" column="order_id"/><result property="orderNo" column="order_no"/><association property="user" javaType="User"><id property="id" column="user_id"/><result property="name" column="user_name"/></association>
</resultMap><select id="selectAllOrdersWithUser" resultMap="OrderWithUserMap">SELECT o.id AS order_id,o.order_no,u.id AS user_id,u.name AS user_nameFROM orders oJOIN user u ON o.user_id = u.id
</select>
✅ 只执行 1 次 SQL,高效且安全。
💡 对于一对多(如订单与订单项),同样适用,只需配合
<collection>使用LEFT JOIN。
🆚 嵌套查询 vs JOIN 查询 对比
| 特性 | 嵌套查询(Nested Select) | JOIN 查询 |
|---|---|---|
| SQL 执行次数 | 1 + N(N+1 问题) | 1 |
| 性能 | 差(尤其 N 很大时) | 优 |
| SQL 复杂度 | 简单(每个查询都很简单) | 稍复杂(需处理列别名、去重) |
| 内存占用 | 低(按需加载) | 高(结果集可能重复膨胀) |
| 适用场景 | 数据量小、懒加载需求 | 绝大多数关联查询场景 |
📌 MyBatis 官方推荐:除非明确需要懒加载(lazy loading),否则优先使用 JOIN +
<association>/<collection>。
🔒 补充:MyBatis 的懒加载(Lazy Loading)
MyBatis 支持通过配置开启懒加载,此时嵌套查询不会立即执行,而是在访问关联属性时才触发子查询:
<!-- mybatis-config.xml -->
<settings><setting name="lazyLoadingEnabled" value="true"/><setting name="aggressiveLazyLoading" value="false"/>
</settings>
这样可以缓解 N+1 问题(因为不是所有关联都会被访问),但一旦访问多个对象的关联属性,仍可能触发多次查询,所以仍需谨慎使用。
✅ 总结
- N+1 问题:1 次主查询 + N 次子查询,性能杀手。
- 根本原因:在映射关联关系时使用了嵌套
select。 - 最佳实践:使用 JOIN 查询 +
<association>/<collection>一次性获取所有数据。 - 例外情况:仅在需要懒加载且关联数据访问频率很低时,才考虑嵌套查询。
🎯 记住一句话:
“能用 JOIN 解决的关联查询,就不要用嵌套 select。”
