HIBERNATE BATCH INSERT VE UPDATE

Hibernate batch insert ile tek tek INSERT ve toplu INSERT akışlarının karşılaştırması

Cuma akşamı 22:00, gece job'u başladı: 10.000 müşteri kaydı veritabanına yazılacak. Saat 22:00:45'te hâlâ devam ediyor. Loglara bakıldığında manzara net — Hibernate her entity için ayrı bir INSERT cümlesi gönderiyor, 10.000 ayrı round-trip. Oysa aynı iş, üç satırlık konfigürasyonla 3 saniyeye iner. Sorun JDBC'de değil, Hibernate'in varsayılan davranışında: hibernate.jdbc.batch_size tanımlanmadığı sürece batch yapmaz, tek tek konuşur.

Varsayılan Davranış Neden Bu Kadar Yavaş?

Hibernate, JPA spesifikasyonunu sıkı uygular: her persist() çağrısı sonrası entity'ye otomatik bir kimlik atanması gerekir. GenerationType.IDENTITY kullanıyorsanız bu kimlik ancak INSERT cümlesi veritabanına gittikten sonra dönebilir — yani Hibernate batch'leyemez bile, çünkü her kayıt için ID'yi beklemek zorunda. Bu yüzden 10.000 satır 10.000 round-trip'e dönüşür.

Aynı yapı GenerationType.SEQUENCE ile çalıştırıldığında tablo değişir. Hibernate ID'leri sequence'den önceden çekip bellekte tutabilir, INSERT cümlelerini gruplayıp tek seferde gönderebilir. İlk yapılacak iş, ID stratejisini doğru seçmektir.

batch_size: İşin Kalbi

application.properties dosyasına eklenmesi gereken minimum satır:

  • spring.jpa.properties.hibernate.jdbc.batch_size=50 — 25 ile 100 arası genellikle ideal aralıktır.
  • spring.jpa.properties.hibernate.order_inserts=true — aynı tablonun INSERT'lerini bir araya getirir.
  • spring.jpa.properties.hibernate.order_updates=true — UPDATE'ler için aynı gruplamayı yapar.
  • spring.jpa.properties.hibernate.jdbc.batch_versioned_data=true — optimistic locking kullanan entity'lerin de batch'e dahil olmasını sağlar.

batch_size=50 demek, Hibernate her 50 INSERT'te bir tek prepared statement gönderecek demektir. 10.000 satır artık 200 round-trip eder. Tek başına bu ayar bile sürenin %80'ini kırpar.

ordered_inserts Neden Kritik?

Şu senaryoyu düşünün: Sipariş, OrderItem, Sipariş, OrderItem... şeklinde kaydediyorsunuz. Hibernate INSERT'leri eklenme sırasına göre gönderir — yani Order INSERT, sonra OrderItem INSERT, sonra tekrar Order INSERT. Aynı tabloya peş peşe gitmediği için batch oluşmaz, her statement tek başına çalışır.

order_inserts=true bu sırayı yeniden düzenler: önce bütün Order'lar tek batch'te, sonra bütün OrderItem'lar tek batch'te gider. JPA ve Hibernate'in batch davranışını derinlemesine anlamak için JPA ve Hibernate eğitiminden yararlanabilirsiniz.

batch_size 50 ile aynı tablo INSERT cümlelerinin tek prepared statement içinde gruplanması

flush() ve clear() Olmadan Bellek Patlar

10.000 entity'yi tek seferde persist() edip işin sonunda commit etmek, persistence context'in bütün nesneleri bellekte tutması anlamına gelir. Heap şişer, GC başlar, performans tersine döner. Doğru desen periyodik temizliktir:

  • Her batch_size kadar entity persist edildikten sonra entityManager.flush() çağır.
  • Hemen ardından entityManager.clear() ile persistence context'i boşalt.
  • Bu döngü hem belleği sabit tutar hem de dirty-checking maliyetini sıfırlar.

Pseudo-kod olarak: for döngüsünde i % 50 == 0 kontrolü yapılır, flush ve clear çağrılır. Bu olmadan batch_size'ı 50 yapmanın anlamı yarıya iner.

UPDATE Tarafı: Daha Sinsi

Update senaryosunda dirty checking devreye girer. Hibernate, attach edilmiş her entity'yi her flush'ta kontrol eder. 10.000 entity'yi yükleyip alanlarını değiştirip kaydetmeye çalışırsanız, bellek sorunu INSERT'ten daha hızlı vurur.

Toplu update için iki yaklaşım çalışır: birincisi entity'leri pencerelere bölüp her pencerede flush/clear yapmak, ikincisi JPQL UPDATE cümlesi yazıp persistence context'i bypass etmek. JPQL bulk update tek SQL üretir, milisaniyelerde biter — ama L2 cache'i tutarsız bırakır, dikkat gerekir.

StatelessSession: Ağır Toplar İçin

Birinci seviye cache, dirty checking, cascade, lifecycle event'ler — bunların hiçbiri ETL benzeri bulk yüklemelerde gerekli değil. Hibernate'in StatelessSession API'si tam burada devreye girer:

  • Persistence context yok — bellek baskısı sıfırlanır.
  • Cascade çalışmaz, ilişkili entity'leri elle insert edersiniz.
  • Interceptor ve event'ler tetiklenmez.
  • JDBC'ye en yakın hız, ama hâlâ Hibernate mapping'leri kullanılır.

Gece job'ları, veri migrasyonu, ilk yükleme senaryoları için ideal. Domain mantığı içeren işlemlerde kullanılmamalı. Konfigürasyon parametrelerinin tam listesi ve sürüm farkları için resmi dokümantasyonu incelemek faydalı olur.

StatelessSession ile EntityManager arasında bellek tüketimi ve flush ve clear ritmi farkı

JDBC Sürücüsünün Rolü

Hibernate konfigürasyonu doğru olsa bile, JDBC sürücüsü batch'i desteklemiyorsa tablo değişmez. PostgreSQL için bağlantı URL'sine reWriteBatchedInserts=true eklenmesi gerekir — bu olmadan PostgreSQL sürücüsü batch'i tek tek INSERT'e dönüştürür. MySQL için rewriteBatchedStatements=true aynı işi görür.

Bu küçük URL parametresi, batch ayarlarının gerçekten round-trip'e dönüşüp dönüşmediğini belirler. p6spy veya Hibernate'in show_sql log'u ile gerçek SQL trafiği doğrulanmalı.

45 saniyelik job'u 3 saniyeye indiren formül üç katmandan oluşur: doğru ID stratejisi, batch_size + ordered_inserts ikilisi ve periyodik flush/clear. Bulk operasyonlar için StatelessSession ve JDBC sürücü parametreleri son rötuşları ekler. Hibernate yavaş değildir — sadece varsayılan ayarları muhafazakârdır.