3.3 一对多关联:collection与结果去重


文档摘要

3.3 一对多关联:collection 与结果去重 本节摘要:一对多关联用 collection 元素缝合,join 会把主表一行展开成多行,框架靠 resultMap 的 id 元素识别同一主对象并合并。本节用"客户带订单列表"病例演示完整写法,做一个"漏写 id 元素导致对象重复"的对照实验,最后拆解 join 与物理分页组合时的行数错位问题。 结果出端的收尾一节,也是整个映射体系里翻车率最高的一节。两个坑——对象重复与分页错位——都是同一个机制的正反面。 病例背景:客户与订单 标准缝法:collection 嵌套结果 SQL 返回的是三行(一个客户乘三张订单),Java 侧得到的是一个 Customer 挂三个 Order。行到对象的折叠由框架完成,而折叠的依据就是 id 元素。

3.3 一对多关联:collection 与结果去重

本节摘要:一对多关联用 collection 元素缝合,join 会把主表一行展开成多行,框架靠 resultMap 的 id 元素识别同一主对象并合并。本节用"客户带订单列表"病例演示完整写法,做一个"漏写 id 元素导致对象重复"的对照实验,最后拆解 join 与物理分页组合时的行数错位问题。

结果出端的收尾一节,也是整个映射体系里翻车率最高的一节。两个坑——对象重复与分页错位——都是同一个机制的正反面。

病例背景:客户与订单

-- 一个客户名下多张订单 CREATE TABLE customers ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, level VARCHAR(10) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, order_no VARCHAR(30) NOT NULL, amount DECIMAL(10,2) ); INSERT INTO customers VALUES (1, '张三', 'VIP'); INSERT INTO orders VALUES (101, 1, 'SO-2026-001', 199.00), (102, 1, 'SO-2026-002', 88.50), (103, 1, 'SO-2026-003', 420.00);
public class Customer { private Integer id; private String name; private String level; private List<Order> orders; // 一对多:订单列表 // 省略 getter 与 setter }

标准缝法:collection 嵌套结果

<resultMap id="customerWithOrdersMap" type="Customer"> <!-- id 元素:框架识别"这是同一个客户"的标记 --> <id property="id" column="c_id"/> <result property="name" column="name"/> <result property="level" column="level"/> <!-- collection:一对多集合,ofType 指明集合元素类型 --> <collection property="orders" ofType="Order"> <id property="id" column="o_id"/> <result property="orderNo" column="order_no"/> <result property="amount" column="amount"/> </collection> </resultMap> <select id="findCustomerWithOrders" parameterType="int" resultMap="customerWithOrdersMap"> SELECT c.id AS c_id, c.name, c.level, o.id AS o_id, o.order_no, o.amount FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE c.id = #{id} </select>
Customer c = mapper.findCustomerWithOrders(1); // 输出:Customer{id=1, name='张三', level='VIP', // orders=[Order{id=101, ...}, Order{id=102, ...}, Order{id=103, ...}]} System.out.println(c); System.out.println(c.getOrders().size()); // 3

SQL 返回的是三行(一个客户乘三张订单),Java 侧得到的是一个 Customer 挂三个 Order。行到对象的折叠由框架完成,而折叠的依据就是 id 元素。

对照实验:把 id 元素换成 result 会怎样

把主表的 id 元素改成普通 result,其余不动:

<!-- 实验版:主表主键降级为普通映射 --> <resultMap id="customerWithOrdersBadMap" type="Customer"> <result property="id" column="c_id"/> <!-- 唯一的改动 --> <result property="name" column="name"/> <result property="level" column="level"/> <collection property="orders" ofType="Order"> <id property="id" column="o_id"/> <result property="orderNo" column="order_no"/> <result property="amount" column="amount"/> </collection> </resultMap>
List<Customer> list = sqlSession.selectList( "com.example.mapper.CustomerMapper.findCustomerWithOrdersBad", 1); System.out.println(list.size()); // 输出:3 —— 期望 1 个客户,实际得到 3 个一模一样的 Customer

实验结果:三行结果被装配成三个 Customer,每个各挂一张订单。

解读:框架逐行装配时,靠 resultMap 的 id 元素判断"这行属于已经出现过的对象吗"。id 元素在,三行被识别为同一客户,订单往同一个列表里追加;id 元素不在(或值错),每行都当新对象建。这就是"订单列表重复""客户查出来三份"这类工单的病根——SQL 没错、数据没错,是折叠标记没画。

03-03-fig01-3

同理可推:内层集合的 id 元素(o_id)如果也漏掉,同一张订单在同一行里不会重复,但跨行合并时无法识别重复订单——当 join 再连一张订单明细表、一行变两行时,订单对象也会重复。内外层的 id 元素都要画准。

分页错位:join 与 LIMIT 的组合病

背景:客户列表页要求每页 5 个客户、带订单数。顺手写法是 join 后 LIMIT:

<!-- 危险写法:join 之后直接物理分页 --> <select id="findPage" resultMap="customerWithOrdersMap"> SELECT c.id AS c_id, c.name, c.level, o.id AS o_id, o.order_no, o.amount FROM customers c LEFT JOIN orders o ON o.customer_id = c.id ORDER BY c.id LIMIT #{offset}, 5 </select>

结果:LIMIT 5 限制的是join 展开后的行。第一个客户若名下有 3 张订单,5 行里只装得下不到两个客户——第二页还没开始,第三个客户的数据已经被截掉一半(订单列表缺张少张)。

解读:物理分页作用于行,而业务分页想作用于客户。两者在 join 之后不再对齐。

解法是让分页发生在折叠之前——先分页主表,再查关联:

<!-- 解法一:子查询先分页主表,再 join 关联 --> <select id="findPageFixed" resultMap="customerWithOrdersMap"> SELECT c.id AS c_id, c.name, c.level, o.id AS o_id, o.order_no, o.amount FROM (SELECT id, name, level FROM customers ORDER BY id LIMIT #{offset}, 5) c LEFT JOIN orders o ON o.customer_id = c.id </select>
<!-- 解法二:嵌套查询,collection 的 select 指向子查询 --> <resultMap id="customerPageMap" type="Customer"> <id property="id" column="id"/> <result property="name" column="name"/> <result property="level" column="level"/> <collection property="orders" ofType="Order" column="id" select="findOrdersByCustomer"/> </resultMap> <select id="findPageByStep" resultMap="customerPageMap"> SELECT id, name, level FROM customers ORDER BY id LIMIT #{offset}, 5 </select> <select id="findOrdersByCustomer" parameterType="int" resultType="Order"> SELECT id, order_no AS orderNo, amount FROM orders WHERE customer_id = #{id} </select>

解法一一条 SQL、分页准确;解法二主查询 1 条加每客户 1 条子查询(5 个客户即 6 条,或用懒加载推迟)。分页中间件(如生态里的 PageHelper)对主查询改写分页,配合解法二天然正确——这也是它成为主流搭配的原因。

⚠️ 常见坑:一对多 resultMap 配 select 星号是雪上加霜——两表列名相撞(都有 id)时折叠标记直接失准。列清单显式写出加别名,是这一节所有写法的隐含前提。

💡 关键直觉:join 把"对象图"压平成"行",映射器的任务是压回去。id 元素是压回去的依据,物理分页是对"行"动刀——凡是"行"与"对象"数量不一致的场景(一对多、多对多),分页都必须放在压平之前。

本节要点回顾

  • collection 嵌套结果:join 宽行加 collection 声明,ofType 指明集合元素类型
  • id 元素是折叠标记:漏写则每行一个新对象,"列表重复"工单的病根
  • 内外层都要画准:主表 id 管对象合并,集合元素 id 管元素去重
  • join 后禁止直接物理分页:LIMIT 作用于行不作用于对象,先分页主表再查关联
  • 两条正路:子查询先分页再 join;或嵌套查询配分页中间件

第 3 章收官:平铺映射、一对一、一对多三种结果缝合术都已上手。下一章转入手术中段——动态 SQL,条件片段如何按需上台、前后缀如何修剪。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U