JPA SERIES - QUAN HỆ

MultipleBagFetchException: Chuông Báo Phép Nhân Dữ Liệu

CARTESIAN PRODUCT - XANH = TẦNG PHÓNG TOHibernate từ chối hai bag fetch đồng thời vì row kết quả không đủ thông tin để dựng lại hai bag có duplicate hợp lệ.

Hai bag fetch

join fetch order.items cùng join fetch order.tags bị chặn lúc dựng SQL AST.

Một cha, hai phép nhânNếu một Order có M items và N tags, SQL trả MxN dòng cho riêng Order đó.

Đổi List thành Set

Exception có thể hết, nhưng database vẫn tạo MxN dòng; chi phí mạng và hydrate không tự biến mất.

Mức 1 - phát hiện ở query translation

BaseSqmToSqlAstConverter

Đếm bag role ngay khi tạo fetch

  1. Visit fetchable associationConverter đọc mapping type và collection semantics.
  2. CollectionClassification.BAGBag không có index hoặc key đủ để phân biệt duplicate membership.
  3. currentBagRoleBag đầu tiên được lưu làm role đang fetch.
  4. Bag thứ hai xuất hiệnConverter đã có một currentBagRole và không tiếp tục.
  5. throw new MultipleBagFetchException(...)Exception liệt kê hai bag role trước khi query nguy hiểm chạy.
Mức 2 - toán ở result set

1 x M x N Rows

Root dedup không xóa chi phí SQL

  1. Order lặp MxN lầnMỗi item ghép với mỗi tag trong join result.
  2. Item lặp theo NCùng item xuất hiện với mọi tag.
  3. Tag lặp theo MCùng tag xuất hiện với mọi item.
  4. Bag cho phép duplicateHibernate không thể biết duplicate nào là dữ liệu thật, duplicate nào do join.
  5. Hibernate 6 dedup root entityDanh sách root sạch hơn Hibernate 5, nhưng row SQL và exception hai bag vẫn còn.
Mức 3 - chữa phép nhân

Tách Fetch Plan

Không bắt một SQL dựng hai collection to-many

  1. Query 1 fetch itemsLoad root và collection thứ nhất theo identity map.
  2. Query 2 fetch tagsCùng managed root được hydrate thêm collection thứ hai.
  3. Batch fetch lazy collectionsDùng nhiều query tuyến tính với IN thay vì một tích Descartes.
  4. Projection theo APITránh dựng aggregate entity khi chỉ cần summary.
  5. Set / @OrderColumnChỉ chọn khi domain thật sự có semantics đó; không dùng để tắt exception.

Phép kiểm: với một root có M = 10 và N = 20, chạy SQL tương đương trực tiếp rồi đếm 200 row. So sánh một join đã đổi sang Set với hai query tách: statement count, rows, bytes, thời gian và heap.

Giảm bậc tăng trưởng

  • Tách query theo collection roleĐể persistence context ráp kết quả theo root identity.
  • Dùng batch fetching khi phù hợpGom lazy debts thành các query tuyến tính.
  • Giữ collection semantics đúng domainList, bag, set và ordered list không hoán đổi chỉ vì performance.

Khi KHÔNG được kết luận

  • Đổi sang Set đã chữa performanceException hết không có nghĩa row multiplication hết.
  • DISTINCT xóa tích DescartesSQL vẫn phải tạo và xử lý row trước khi dedup.
  • Hai query luôn chậm hơn một querySố round trip nhỏ hơn không thắng được lượng row tăng theo tích.