İçeriğe geç

join fetch ve Sayfalama — 10 Sipariş İçin Bütün Tablo

Orta 9 dk Sık karşılaşılır

Önce şunu oku: Flush ve SQL Sırası — save() Satırında Neden Hiçbir Şey Olmuyor?

30 saniyede özet

Koleksiyonu join fetch ile çekip aynı sorguyu sayfalarsan Hibernate LIMIT'i SQL'den çıkarır ve sayfayı bellekte keser. Doğrusu iki küçük sorgu: önce sayfanın id'leri, sonra yalnızca onların detayı.

Ekranda yalnızca on sipariş var. Testte her şey hızlıydı, canlıda sayfa saniyelerce açılmıyor. Bellek grafiği her tıklamada sıçrıyor.

  1. Bayt: Siparişleri satırlarıyla birlikte, sayfa sayfa gösteriyorum. Tek sorgu, çok şık.

  2. Sen: Testte kaç sipariş vardı?

  3. Bayt: Elli. Canlıda on bin... ama ben yine on tane istiyorum!

  4. Bayt: Sen on tane istiyorsun. Veritabanı kaç tane gönderiyor, ona bakalım.

Sayfayı veritabanı keser

LIMIT 10 OFFSET 20 veritabanına “yirmi satırı atla, on tane ver” der. Kesme işini veritabanı yapar ve ağdan yalnızca on satır geçer.

Ama siparişi satırlarıyla birlikte çekmek için tabloları birleştirirsin. Bu birleştirmede bir sipariş, satır sayısı kadar sonuç satırı üretir. On sonuç satırı artık on sipariş demek değildir.

Kafam karıştı, daha basit anlat

Veritabanı on satır keser, sipariş saymaz. Üç satırlık bir sipariş üç sonuç satırı tutar.

Hızlı kontrolBaşlangıç

LIMIT 10 OFFSET 20 veritabanına ne söyler?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Bir sipariş 3 satır (line) içeriyor. orders JOIN order_lines sorgusunda bu sipariş kaç sonuç satırı üretir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

join fetch sayfalamayla buluşunca

select o from Order o join fetch o.lines sorgusuna setFirstResult(20) ve setMaxResults(10) verdin. Tabloda 10.000 sipariş var. Hibernate SQL'e ne yazar? Cevabı göster

Hiç LIMIT yazmaz. Sonuç satırlarını sipariş sınırında kesemeyeceği için bütün siparişleri satırlarıyla getirir, sonra on tanesini bellekte ayırır. Log’a yalnızca bir uyarı düşer.

On kitap istedin, bütün katalog geldi.
Adım adım oku
  1. Sayfa 3 istendi: on sipariş ve satırları.
  2. LIMIT SQL'den çıkarıldı; bütün siparişler bütün satırlarıyla geldi.
  3. On tanesi bellekte ayrıldı, gerisi atıldı.
  4. Doğru yol: önce on id'yi LIMIT ile al, sonra yalnızca onları getir.

fetch joinJPQL'de join fetch: ilişkili entity ya da koleksiyonu aynı sorguda doldurur. Koleksiyonla birlikte sayfalanırsa LIMIT düşer ve sayfa bellekte kesilir.Sözlükte gör → bir koleksiyonu tek sorguda doldurur. Sayfalamayla birleşince Hibernate HHH90003004 uyarısını yazar: “applying in memory”. Hata yok, sadece yavaş ve aç gözlü bir sorgu.

hibernate.query.fail_on_pagination_over_collection_fetch=true ayarı bu uyarıyı hataya çevirir. Sorun böylece ilk testte görünür.

Kafam karıştı, daha basit anlat

join fetch ve sayfalama aynı sorguda buluşursa sayfa bellekte kesilir. Ayarı açarsan Hibernate bunu yapmak yerine hata verir.

Hızlı kontrolOrta

join fetch o.lines olan bir sorguya setMaxResults(10) verildi. Hibernate ne yapar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

hibernate.query.fail_on_pagination_over_collection_fetch=true ne işe yarar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

İki küçük sorgu

Birinci sorgu yalnızca sayfadaki siparişlerin id’lerini LIMIT ile alır. İkinci sorgu o id’ler için join fetch yapar. Kesme veritabanında kalır ve yalnızca gereken satırlar gelir.

Diğer yol batch fetchingTembel koleksiyon ya da ilişkileri tek tek değil, birkaçını bir IN sorgusuyla yükleme. default_batch_fetch_size ya da @BatchSize ile açılır; N+1'i küçültür.Sözlükte gör →: sayfayı tembel yüklersin, satırlara ilk dokunuşta Hibernate on siparişin satırlarını tek bir IN sorgusuyla getirir. Bu, N+1Bir sorgu listeyi getirir, sonra her eleman için bir sorgu daha atılır. Her biri hızlı olduğu için yavaş sorgu logunda görünmez; yavaşlayan şey toplam gidiş-dönüş sayısıdır.Sözlükte gör → sorununu da iki sorguya indirir.

Kafam karıştı, daha basit anlat

Önce on numarayı al, sonra yalnızca o on siparişi satırlarıyla getir. İki kısa yolculuk, bir dev yolculuktan iyidir.

Hızlı kontrolOrta

Siparişleri satırlarıyla sayfalamanın doğru iki sorgulu yolu hangisi?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Sayfa 3, satırlarıyla birlikte

Tohum 272354

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

join fetch + sayfalama

Hedef hep aynı: 3. sayfadaki 10 sipariş ve her birinin satırları.

Görevler0/3

  • 10 sipariş için 10.000 siparişi belleğe yükleaçık

    İpucu

    Tablo büyüklüğünü değiştir.

  • Aynı sayfayı 11 sorguyla getiraçık

    İpucu

    Hiçbir şeyi önceden yükleme.

  • 10.000 siparişlik tabloda sayfayı 2 sorguyla, bellekte kesmeden getiraçık

    İpucu

    İki yol var.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. LIMIT yok, 50 siparişin hepsi belleğe geldi.
  2. Tabloyu 10.000 yap. Aynı on sipariş için on bin sipariş kuruldu.
  3. “Bellekte sayfalamayı yasakla”yı aç. Sorgu çalışmadan hata verdi.
  4. Tembel yüklemeyi seç. Doğru sonuç ama 11 sorgu.
  5. Batch fetch’i ve iki adımlı yolu dene. İkisi de iki sorgu, satır sayısı tablo büyüdükçe değişmiyor.
Hızlı kontrolOrta

Sayfa tembel yükleniyor ve default_batch_fetch_size=16. 10 siparişin satırlarına dokunulduğunda kaç sorgu çalışır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

Spring Data’da gizli hâli. Page<Order> findAll(Pageable) üstüne koleksiyon için @EntityGraph koymak da aynı bellekte sayfalamayı yapar.

İki List’i birden fetch etmek. İki List koleksiyonunu aynı sorguda join fetch edersen MultipleBagFetchException alırsın. Set’e çevirmek hatayı susturur ama satırları çarpar: 5 satır ve 4 ödeme, sipariş başına 20 satır eder.

IN sorgusu sırayı korumaz. İkinci sorguya da aynı order by’ı yaz; yoksa sayfa karışık sırada gelir.

Kafam karıştı, daha basit anlat

Her sorguda tek bir koleksiyonu fetch et, sıralamayı iki sorguya da yaz.

Hızlı kontrolOrta

Order'ın iki List koleksiyonu var: lines ve payments. İkisini aynı sorguda join fetch edince ne olur?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aşağıdaki örnek bir bankanın müşteri listesi ekranından: her müşteri kartlarıyla birlikte, sayfa sayfa.

Derinleş · Müşteri listesi: önce id sayfası, sonra kartlarıyla müşteriler 5 dosya · ~85 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/customer/ Customer.java Müşteri ve kartları: tek bir koleksiyon, varsayılan olarak tembel.

src/main/java/com/bank/customer/Customer.java
@Entity
@Table(name = "customers")
class Customer {
@Id
private Long id;
private String fullName;
private Instant createdAt;
@OneToMany(mappedBy = "customer")
private Set<Card> cards = new HashSet<>(); // lazy by default for collections
protected Customer() {
}
Long id() {
return id;
}
Set<Card> cards() {
return Collections.unmodifiableSet(cards);
}
}

src/main/java/com/bank/customer/ CustomerRepository.java İki sorgu: LIMIT'li id sayfası ve o id'ler için join fetch. İkisinde de aynı sıralama.

src/main/java/com/bank/customer/CustomerRepository.java
interface CustomerRepository extends JpaRepository<Customer, Long> {
// Step 1: only ids, so LIMIT/OFFSET stays in the SQL. Spring Data adds the count query.
@Query("select c.id from Customer c order by c.createdAt, c.id")
Page<Long> findIdPage(Pageable pageable);
// Step 2: the details of exactly those customers. IN does not keep order,
// so the same ORDER BY is repeated here.
@Query("""
select c from Customer c
left join fetch c.cards
where c.id in :ids
order by c.createdAt, c.id
""")
List<Customer> findWithCards(Collection<Long> ids);
}

src/main/java/com/bank/customer/ CustomerListing.java Servis id sayfasını alıp detayla birleştiriyor; toplam sayı birinci sorgudan geliyor.

src/main/java/com/bank/customer/CustomerListing.java
@Service
class CustomerListing {
private final CustomerRepository customers;
CustomerListing(CustomerRepository customers) {
this.customers = customers;
}
@Transactional(readOnly = true)
public Page<Customer> page(int number, int size) {
Page<Long> ids = customers.findIdPage(PageRequest.of(number, size));
if (ids.isEmpty()) {
return Page.empty(ids.getPageable());
}
List<Customer> rows = customers.findWithCards(ids.getContent());
return new PageImpl<>(rows, ids.getPageable(), ids.getTotalElements());
}
}

src/main/resources/ application.yml Bellekte sayfalamayı hataya çeviren ayar ve tembel koleksiyonlar için batch fetch.

src/main/resources/application.yml
spring:
jpa:
properties:
hibernate:
query:
# join fetch + paging in one query now throws instead of loading the table
fail_on_pagination_over_collection_fetch: true
# lazy collections touched in a loop load in groups of up to 16
default_batch_fetch_size: 16

src/test/java/com/bank/customer/ CustomerListingTest.java Test: ikinci sayfada tam on müşteri, kartlarıyla ve doğru sırada.

src/test/java/com/bank/customer/CustomerListingTest.java
@DataJpaTest
@Import(CustomerListing.class)
@Sql("/customers-25-with-cards.sql") // 25 customers, created in id order
class CustomerListingTest {
@Autowired CustomerListing listing;
@Test
void secondPageHasTenCustomersInOrderWithTheirCards() {
Page<Customer> page = listing.page(1, 10);
assertThat(page.getTotalElements()).isEqualTo(25);
assertThat(page.getContent()).extracting(Customer::id)
.containsExactly(11L, 12L, 13L, 14L, 15L, 16L, 17L, 18L, 19L, 20L);
assertThat(page.getContent()).allSatisfy(c -> assertThat(c.cards()).isNotEmpty());
}
}

Kendini sına

Şimşek turu1/4

Koleksiyon join fetch'i ile sayfalama yapılınca SQL'e yine LIMIT yazılır.

Soru 1/3İleri

lines ve payments Set'e çevrildi, ikisi aynı sorguda join fetch ediliyor. Bir siparişin 5 line'ı ve 4 payment'ı var. Bu sipariş kaç sonuç satırı üretir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Koleksiyon join fetch'i ile sayfalama aynı sorguda olursa LIMIT SQL'den düşer. Bütün tablo yüklenir ve sayfa bellekte kesilir; log'daki tek iz HHH90003004 uyarısıdır.
  2. 2 Doğru yol iki sorgudur: önce LIMIT'li bir id sorgusu, sonra o id'ler için join fetch. Batch fetching de aynı işi iki sorguda görür.
  3. 3 fail_on_pagination_over_collection_fetch ayarı bu uyarıyı hataya çevirir. Açık tutulursa sorun üretimde değil, ilk testte görünür.
Sonraki kapı Kart, havale ve fatura ödemesi: üçü de Payment. Veritabanında kaç tablo olmalı? Kalıtım Stratejileri — Üç Ödeme Türü, Kaç Tablo? · 9 dk

4 kart sonraki derste seni bekliyor

0/4 kart bu dersten toplandı

Bu dersin üstüne kurulanlar

Bunlar bu dersi temel alıyor; hazır olduğunda devam edebilirsin.