← 返回题目列表

MyBatis 的 association 和 collection 怎么做一对一、一对多映射?嵌套查询和嵌套结果有什么区别?

高频 困难 第 14 / 24 题 更新于 2026/08/03
associationcollection嵌套映射一对多

简化版

**MyBatis 用 resultMap 里的 <association><collection> 处理「对象里嵌套对象」的映射:<association> 处理「一对一」(一个订单对应一个用户,订单对象里嵌一个 User),<collection> 处理「一对多」(一个订单对应多个订单项,订单对象里嵌一个 List)。**每种又有两种实现方式:① 嵌套结果(Nested Results)——用一条 SQL(join)把主表和关联表一次查出来,在 resultMap 里把结果集拆分映射到主对象和嵌套对象;② 嵌套查询(Nested Select)——主查询查主表,对每条主记录再执行一条子查询去查关联数据(select 属性指向另一个查询)。两者关键区别:嵌套结果是「一条 join SQL」(1 次查询),嵌套查询是「多条 SQL」(查出 N 条主记录就再执行 N 次子查询,即 N+1 问题)。所以嵌套结果(join)性能通常更好(一次查询),嵌套查询会有 N+1 问题(但可配合懒加载「用到才查」)。选择:一次性要用关联数据、想避免 N+1 → 嵌套结果(join);关联数据不一定用到、想懒加载 → 嵌套查询。

详细版

association vs collection、嵌套结果 vs 嵌套查询

维度associationcollection
关系一对一(嵌一个对象)一对多(嵌一个 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 里有 UserList<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 指元素类型、靠 标识主对象唯一性把多行组装成一个 List( 很重要)」,就掌握了 collection。

四、嵌套结果:一条 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,靠组装多行成 List);嵌套结果=一条 join SQL(无 N+1 性能好但不能懒加载)vs 嵌套查询=主查询+每条再子查询(N+1 但支持懒加载);总用关联数据用 join、用不到用嵌套查询+懒加载,避免 N+1 靠 join/懒加载/批量查询」。