MyBatis 的 association 和 collection 怎么做一对一、一对多映射?嵌套查询和嵌套结果有什么区别?
简化版
**MyBatis 用 resultMap 里的 <association> 和 <collection> 处理「对象里嵌套对象」的映射:<association> 处理「一对一」(一个订单对应一个用户,订单对象里嵌一个 User),<collection> 处理「一对多」(一个订单对应多个订单项,订单对象里嵌一个 ListresultMap 里把结果集拆分映射到主对象和嵌套对象;② 嵌套查询(Nested Select)——主查询查主表,对每条主记录再执行一条子查询去查关联数据(select 属性指向另一个查询)。两者关键区别:嵌套结果是「一条 join SQL」(1 次查询),嵌套查询是「多条 SQL」(查出 N 条主记录就再执行 N 次子查询,即 N+1 问题)。所以嵌套结果(join)性能通常更好(一次查询),嵌套查询会有 N+1 问题(但可配合懒加载「用到才查」)。选择:一次性要用关联数据、想避免 N+1 → 嵌套结果(join);关联数据不一定用到、想懒加载 → 嵌套查询。
详细版
association vs collection、嵌套结果 vs 嵌套查询:
| 维度 | association | collection |
|---|---|---|
| 关系 | 一对一(嵌一个对象) | 一对多(嵌一个 List) |
| 例子 | 订单→用户 | 订单→多个订单项 |
| 实现方式 | 嵌套结果(Nested Results) | 嵌套查询(Nested Select) |
|---|---|---|
| SQL 数量 | 1 条(join) | 1 + N 条(N+1 问题) |
| 配置 | 用 join SQL + resultMap 拆分 | select 属性指向另一查询 |
| 性能 | 通常更好(一次查询) | 有 N+1(可懒加载缓解) |
| 懒加载 | 不适用 | 支持(用到才查) |
<!-- 嵌套结果:一条 join SQL,resultMap 拆分映射 -->
<resultMap id="orderMap" type="Order">
<id column="order_id" property="id"/>
<result column="amount" property="amount"/>
<!-- 一对一:association 映射 join 出来的 user 字段 -->
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
</association>
<!-- 一对多:collection 映射 join 出来的多个 item 行 -->
<collection property="items" ofType="OrderItem">
<id column="item_id" property="id"/>
<result column="item_name" property="name"/>
</collection>
</resultMap>
<select id="getOrder" resultMap="orderMap">
SELECT o.id order_id, o.amount, u.id user_id, u.name user_name,
i.id item_id, i.name item_name
FROM orders o JOIN users u ON o.user_id=u.id
LEFT JOIN order_items i ON i.order_id=o.id
WHERE o.id = #{id}
</select>
<!-- 嵌套查询:主查询 + 子查询(有 N+1,可懒加载)-->
<resultMap id="orderMap2" type="Order">
<id column="id" property="id"/>
<association property="user" column="user_id"
select="com.x.UserMapper.selectById"/> <!-- 每条订单再查一次 user -->
<collection property="items" column="id"
select="com.x.ItemMapper.selectByOrderId"/> <!-- 每条订单再查一次 items -->
</resultMap>
⚠️ 嵌套查询(Nested Select)是 N+1 问题的直接来源,一定要警惕:查 100 个订单(1 条 SQL),如果每个订单再触发一条「查该订单的用户」和一条「查该订单的订单项」,就变成了
1 + 100 + 100 = 201条 SQL——这就是 N+1(见 lazy-loading-n-plus-one 题)。嵌套结果(join)用一条 SQL 解决,性能通常好得多。但嵌套查询也有它的价值:配合懒加载(fetchType="lazy"),关联对象「用到才查」——如果很多时候不访问order.getUser(),那就不会触发那次子查询,反而更省。所以选择的核心是:如果你几乎总是要用关联数据,用嵌套结果(join,避免 N+1);如果关联数据经常用不到,用嵌套查询 + 懒加载(用到才查)。
完整版教学
一、问题:对象里嵌套对象怎么映射
先理解「嵌套映射」要解决什么:
Java 对象常常"嵌套"其他对象:
class Order {
Long id;
BigDecimal amount;
User user; // 一对一:一个订单属于一个用户
List<OrderItem> items; // 一对多:一个订单有多个订单项
}
数据库是"扁平的表":orders、users、order_items 三张表
查出来的结果集也是"扁平的行"
问题:怎么把"扁平的查询结果",映射成"嵌套的对象结构"?
(把 user 相关的列组装成 User 对象、
把多行 item 组装成 List<OrderItem>)
MyBatis 的方案:resultMap 里用
<association>:映射"一对一"的嵌套对象(user)
<collection>:映射"一对多"的嵌套集合(items)
嵌套映射要解决「把扁平的查询结果映射成嵌套的对象结构」——Java 对象常嵌套其他对象(Order 里有 User 和 List<OrderItem>),但数据库是扁平的表和行。MyBatis 用 resultMap 里的 <association>(映射一对一嵌套对象)和 <collection>(映射一对多嵌套集合) 来做这个组装。理解「嵌套映射解决扁平结果映射成嵌套对象、association 映射一对一对象、collection 映射一对多集合」,就理解了嵌套映射的目标。
二、association:一对一映射
<association> 处理「一对一」——对象里嵌一个对象:
一对一场景:一个订单对应一个用户
class Order { User user; } // 嵌一个 User 对象
<association property="user" javaType="User">
<id column="user_id" property="id"/>
<result column="user_name" property="name"/>
</association>
含义:把结果集里 user_id、user_name 这些列
组装成一个 User 对象,赋给 order.user
关键:
property:主对象里的属性名(user)
javaType:嵌套对象的类型(User)
内部的 <id>/<result>:怎么把列映射到 User 的属性
所以 association = "一对一嵌套对象的映射规则"
<association> 处理「一对一」——对象里嵌一个对象(Order 里的 User)。配置:property(主对象的属性名)、javaType(嵌套对象类型)、内部 <id>/<result>(列到嵌套对象属性的映射)。它把结果集里的 user 相关列组装成一个 User 对象赋给 order.user。理解「association 处理一对一、嵌一个对象、property+javaType+内部映射列到嵌套对象属性」,就掌握了 association。
三、collection:一对多映射
<collection> 处理「一对多」——对象里嵌一个集合:
一对多场景:一个订单有多个订单项
class Order { List<OrderItem> items; } // 嵌一个 List
<collection property="items" ofType="OrderItem">
<id column="item_id" property="id"/>
<result column="item_name" property="name"/>
</collection>
含义:把结果集里多行的 item 数据,组装成一个 List<OrderItem>
赋给 order.items
关键:
property:主对象里的集合属性名(items)
ofType:集合元素的类型(OrderItem,注意是 ofType 不是 javaType)
内部 <id>/<result>:怎么把列映射到 OrderItem
collection 怎么把"多行"组装成"一个 List"(去重的关键):
用 <id> 标识主对象的唯一性
→ join 后一个订单对应多行(每行一个 item)
→ MyBatis 靠订单的 <id> 判断"这些行是同一个订单"
→ 把它们的 item 组装进同一个订单的 items 列表
★ 所以 <id> 很重要(没配 id 可能导致重复/组装错误)
<collection> 处理「一对多」——对象里嵌一个集合(Order 里的 List<OrderItem>)。配置:property(集合属性名)、ofType(集合元素类型,注意是 ofType 不是 javaType)、内部映射。它把多行 item 组装成一个 List ——靠主对象的 <id> 标识唯一性(join 后一个订单对应多行,MyBatis 靠订单的 <id> 判断「这些行是同一订单」,把 item 组装进同一订单的列表)。<id> 很重要(没配可能重复/组装错)。理解「collection 处理一对多、嵌一个集合、ofType 指元素类型、靠
四、嵌套结果:一条 join SQL
第一种实现方式「嵌套结果」——用一条 join SQL:
嵌套结果(Nested Results):
用一条 SQL(join 关联表),把主表和关联表数据一次查出来
再在 resultMap 里把扁平的结果集"拆分"到主对象和嵌套对象
SELECT o.id, o.amount, u.id user_id, u.name user_name,
i.id item_id, i.name item_name
FROM orders o
JOIN users u ON o.user_id = u.id
LEFT JOIN order_items i ON i.order_id = o.id
WHERE o.id = #{id}
→ 一条 SQL 查出所有数据(订单 + 用户 + 订单项)
→ resultMap 的 association/collection 把 user_*、item_* 列
组装成 User 对象和 items 列表
特点:
✓ 只有 1 条 SQL → 没有 N+1 问题
✓ 性能通常更好(数据库一次 join 拿全)
✗ join 可能有数据冗余(一对多时主表数据在多行重复)
✗ 不支持懒加载(一次全查出来了)
适合:几乎总是要用关联数据的场景
第一种实现「嵌套结果(Nested Results)」——用一条 join SQL 把主表和关联表一次查出,resultMap 的 association/collection 把扁平结果集拆分组装。特点:只有 1 条 SQL(无 N+1)、性能通常更好(一次 join 拿全);缺点是 join 可能有数据冗余(一对多时主表在多行重复)、不支持懒加载。适合几乎总是要用关联数据的场景。理解「嵌套结果=一条 join SQL 拆分映射、只有 1 条 SQL 无 N+1 性能好、缺点是冗余和不支持懒加载、适合总要用关联数据」,就掌握了嵌套结果方式。
五、嵌套查询:多条 SQL 与 N+1
第二种实现方式「嵌套查询」——多条 SQL,会 N+1:
嵌套查询(Nested Select):
主查询查主表,对每条主记录再执行一条子查询查关联数据
<association property="user" column="user_id"
select="UserMapper.selectById"/>
→ 主查询查订单后,对每个订单的 user_id 再调 selectById 查用户
<collection property="items" column="id"
select="ItemMapper.selectByOrderId"/>
→ 对每个订单的 id 再调 selectByOrderId 查订单项
★ N+1 问题:
查 100 个订单(1 条 SQL)
+ 对每个订单查 user(100 条)
+ 对每个订单查 items(100 条)
= 201 条 SQL!
特点:
✗ N+1 问题(SQL 数量随主记录数增长)
✓ 支持懒加载(fetchType="lazy")——关联数据用到才查
→ 如果经常用不到 user/items,就不会触发那些子查询
适合:关联数据不一定用到(懒加载能省查询)
第二种实现「嵌套查询(Nested Select)」——主查询查主表,对每条主记录再执行一条子查询(select 属性指向另一个查询)。N+1 问题:查 100 个订单(1 条)+ 每个查 user(100 条)+ 每个查 items(100 条)= 201 条 SQL。特点:有 N+1(SQL 随主记录数增长),但支持懒加载(fetchType="lazy",关联数据用到才查——如果经常用不到就不触发子查询)。适合关联数据不一定用到(懒加载能省查询)。理解「嵌套查询=主查询+对每条记录再子查询(select 属性)、有 N+1(1+N 条 SQL)但支持懒加载(用到才查)、适合关联数据不一定用到」,就掌握了嵌套查询方式。
六、如何选择:嵌套结果 vs 嵌套查询
综合对比,给出选择原则:
核心权衡:SQL 数量 vs 懒加载能力
嵌套结果(join):
✓ 1 条 SQL,无 N+1,性能通常好
✗ 不能懒加载,join 有数据冗余(一对多尤其)
→ 选它当:几乎总是要用关联数据、想避免 N+1、关联数据量不大
嵌套查询(select):
✗ N+1 问题(不懒加载时性能差)
✓ 支持懒加载(用到才查,用不到就省)
→ 选它当:关联数据经常用不到、想懒加载
实践建议:
① 明确会用关联数据 → 嵌套结果(join),避免 N+1
② 关联数据可能用不到 → 嵌套查询 + 懒加载
③ 列表页要展示关联数据、量大 → 嵌套结果(一次 join)
或手动"批量查询"(先查主表,再用 IN 批量查关联,自己组装)
→ 批量查询是 N+1 的另一种优化(2 条 SQL 而非 N+1)
避免 N+1 的三招:
join(嵌套结果)、懒加载(用不到就不查)、批量查询(IN 一次查完)
选择的核心权衡是「SQL 数量 vs 懒加载能力」:嵌套结果(join)——1 条 SQL 无 N+1 性能好,但不能懒加载、有冗余,选它当几乎总要用关联数据、想避免 N+1;嵌套查询(select)——有 N+1 但支持懒加载,选它当关联数据经常用不到。避免 N+1 的三招:join(嵌套结果)、懒加载(用不到不查)、批量查询(先查主表再用 IN 一次查关联、2 条 SQL 而非 N+1)。理解「选择权衡 SQL 数量 vs 懒加载:总用关联数据用嵌套结果(join)避免 N+1、关联数据用不到用嵌套查询+懒加载、避免 N+1 三招 join/懒加载/批量查询」,就掌握了选择原则。
记忆钩子:「MyBatis 嵌套映射:
处理一对一(嵌一个对象,property+javaType)、 。处理一对多(嵌一个 List,ofType 指元素类型,靠 标识主对象唯一性把多行组装成 List);两种实现:①嵌套结果(Nested Results,一条 join SQL 拆分映射,只 1 条 SQL 无 N+1 性能好,但不能懒加载有冗余)②嵌套查询(Nested Select,主查询+对每条记录再子查询 select 属性,有 N+1=1+N 条 SQL,但支持懒加载用到才查);选择:总用关联数据用嵌套结果避 N+1、用不到用嵌套查询+懒加载;避免 N+1 三招:join/懒加载/批量查询(IN)」
七、常见误区与追问
- 误区:association 和 collection 是一回事。 association 处理一对一(嵌一个对象,如订单→用户),collection 处理一对多(嵌一个 List,如订单→多个订单项,用 ofType 指元素类型);一个映射单个对象、一个映射集合。
- 误区:嵌套查询和嵌套结果性能差不多。 差很多——嵌套结果用一条 join SQL(1 次查询);嵌套查询对每条主记录再查一次(1+N 条 SQL,N+1 问题);查 100 条主记录,嵌套查询可能变成 201 条 SQL,性能差很多(除非懒加载且关联数据用不到)。
- 误区:collection 映射不配
也没事。 很重要——它标识主对象的唯一性;一对多 join 后一个主对象对应多行,MyBatis 靠 判断「这些行属于同一主对象」把关联项组装进同一个集合;不配 可能导致重复或组装错误。 - 误区:嵌套查询一定比嵌套结果差。 不一定——嵌套查询支持懒加载(fetchType=“lazy”),如果关联数据经常用不到(不访问 order.getUser()),就不会触发那些子查询,反而比 join 全查出来更省;关键看关联数据是否经常用到。
- 追问:MyBatis 的 N+1 问题是怎么产生的?怎么避免? 嵌套查询(Nested Select)产生——查 N 条主记录(1 条 SQL)后,对每条再执行一条子查询查关联数据(N 条),共 1+N 条 SQL;避免三招:用嵌套结果(一条 join SQL)、用懒加载(用不到就不查)、或手动批量查询(先查主表再用 IN 一次查所有关联数据、自己组装,2 条 SQL)。
- 追问:collection 用 join(嵌套结果)时,为什么主表数据会「重复」? 一对多 join 时,一个主记录关联多条子记录,结果集里主记录的数据会在多行重复出现(每行带一个不同的子记录);MyBatis 靠主对象的
识别这些行属于同一主对象,把它们的子记录组装进同一个集合、主对象只创建一个,从而消除重复。 - 追问:association/collection 的 javaType 和 ofType 有什么区别? association 用 javaType 指定嵌套对象的类型(单个对象,如 User);collection 用 ofType 指定集合「元素」的类型(如 OrderItem,因为集合本身类型是 List,要指明装的是什么)——这是两者配置上的一个关键区别。
八、加强记忆
MyBatis 用 resultMap 里的 <association> 和 <collection> 处理嵌套对象映射:<association> 处理一对一(对象里嵌一个对象,如订单→用户,用 property+javaType);<collection> 处理一对多(对象里嵌一个 List,如订单→多个订单项,用 ofType 指元素类型,靠主对象的 <id> 标识唯一性把多行组装成一个 List,<id> 很重要)。每种有两种实现:① 嵌套结果(Nested Results)——用一条 join SQL 把主表和关联表一次查出,resultMap 拆分映射;只 1 条 SQL、无 N+1、性能通常好,但不能懒加载、一对多 join 有数据冗余;② 嵌套查询(Nested Select)——主查询查主表,对每条主记录再执行一条子查询(select 属性指向另一查询);有 N+1 问题(1+N 条 SQL),但支持懒加载(fetchType="lazy",用到才查)。选择:几乎总是要用关联数据、想避免 N+1 → 嵌套结果(join);关联数据经常用不到 → 嵌套查询 + 懒加载。避免 N+1 三招:join(嵌套结果)、懒加载(用不到不查)、批量查询(先查主表再 IN 一次查关联,2 条 SQL)。一句话「association 一对一(javaType)、collection 一对多(ofType,靠