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
- Visit fetchable associationConverter đọc mapping type và collection semantics.
CollectionClassification.BAGBag không có index hoặc key đủ để phân biệt duplicate membership.currentBagRoleBag đầu tiên được lưu làm role đang fetch.- Bag thứ hai xuất hiệnConverter đã có một
currentBagRolevà không tiếp tục. 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
- Order lặp MxN lầnMỗi item ghép với mỗi tag trong join result.
- Item lặp theo NCùng item xuất hiện với mọi tag.
- Tag lặp theo MCùng tag xuất hiện với mọi item.
- Bag cho phép duplicateHibernate không thể biết duplicate nào là dữ liệu thật, duplicate nào do join.
- 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
- Query 1 fetch
itemsLoad root và collection thứ nhất theo identity map. - Query 2 fetch
tagsCùng managed root được hydrate thêm collection thứ hai. - Batch fetch lazy collectionsDùng nhiều query tuyến tính với
INthay vì một tích Descartes. - Projection theo APITránh dựng aggregate entity khi chỉ cần summary.
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.
DISTINCTxó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.