JPA SERIES - QUAN HỆ

N+1: Từng Lazy Load Đều Đúng, Tổng Thể Vẫn Thảm Họa

QUERY SHAPE - XANH = TẦNG PHÓNG TOĐếm statement theo use case. Fetch strategy là thiết kế query, không phải thuộc tính trang trí trên entity.

Nhiều query nhỏ

Một query lấy N cha; vòng lặp chạm lazy association và phát thêm tối đa N query cho mỗi role.

Hibernate không thấy vòng lặp tương laiMỗi lazy access là một yêu cầu hợp lệ tại thời điểm gọi; fetch plan ban đầu mới quyết định tổng chi phí.

Một query quá lớn

Join fetch nhiều collection hoặc collection kèm pagination tạo row multiplication và có thể paginate trong memory.

Mức 1 - query gốc

Load N Entity Cha

Persistence context nhận entity và các lazy placeholder

  1. findByShopId(...)Một SQL trả danh sách Order.
  2. Identity map khử trùng entityMỗi DB identity có một managed instance trong Session.
  3. Gắn proxy / collection wrapperAssociation lazy chưa có dữ liệu, chỉ giữ owner key và Session.
  4. Query gốc kết thúcHibernate chưa biết caller sẽ chạm role nào cho bao nhiêu phần tử.
  5. Vòng lặp bắt đầuChi phí ẩn xuất hiện sau query gốc, đôi khi ở mapper hoặc serializer.
Mức 2 - mỗi lần trả nợ

DefaultInitializeCollectionEventListener

Một wrapper chưa initialized đi vào loader

  1. AbstractPersistentCollection.initialize()Getter collection yêu cầu Session load dữ liệu.
  2. onInitializeCollection(event)Lấy CollectionEntry, persister và loaded key.
  3. Thử second-level cacheCache hit có thể tránh SQL; cache miss đi tiếp xuống loader.
  4. loadedPersister.initialize(key, session)Không batch fetch thì mỗi owner key thường thành một SELECT.
  5. Persistence context nhớ kết quảChạm lại cùng wrapper không query lại, nhưng N owner khác vẫn còn N món nợ.
Mức 3 - thiết kế fetch plan

Join, Batch Hoặc Hai Pha

Không có một annotation đúng cho mọi endpoint

Danh sách không phân trang hoặc cardinality nhỏ

  • Fetch join / EntityGraph có chọn lọcGộp association cần dùng; tránh join đồng thời nhiều collection to-many.
  • Projection DTOChỉ lấy đúng cột và shape API cần.

Pagination hoặc nhiều collection

  • Hai pha: page IDs rồi fetch graphGiữ pagination ở SQL và tách phép nhân row.
  • default_batch_fetch_sizeGom nhiều owner key vào IN (...) thay vì từng query.
  • fail_on_pagination_over_collection_fetch=trueBiến cảnh báo HHH90003004 thành lỗi thay vì paginate collection fetch trong memory.

Phép kiểm: bật Hibernate statistics hoặc datasource proxy, chạy endpoint với N = 1, 10, 50 và ghi số prepared statements, rows, thời gian, heap. Một regression test nên assert query count hoặc query shape, không chỉ assert JSON đúng.

Thiết kế theo use case

  • Vẽ association thật sự cần đọcChọn projection/fetch graph riêng cho từng API.
  • Tách pagination khỏi collection fetchPage root IDs trước, fetch dữ liệu sau.
  • Giữ cảnh báo thành lỗi ở testKhông để in-memory pagination đi qua im lặng.

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

  • EAGER tự xóa N+1EAGER là yêu cầu availability, không hứa một SQL hay join fetch.
  • Fetch join luôn nhanh hơnRow multiplication, băng thông và heap có thể lớn hơn nhiều query tuyến tính.
  • OSIV là nguyên nhân duy nhấtN+1 vẫn xảy ra trong transaction; OSIV chỉ mở thêm nơi nó có thể ẩn.