HIBERNATE BATCH INSERT VE UPDATE

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.
1. 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.
2. 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.
3. 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.

4. 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.
5. 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.
6. 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.

7. 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.
Sıkça Sorulan Sorular
batch_size'ı 1000 yapsam daha mı hızlı olur?
Genellikle hayır. 25-100 aralığı çoğu veritabanında optimum sonucu verir. Çok yüksek değerler prepared statement'ı dev boyuta çıkarır, network paketleri parçalanır, veritabanı tarafında parse maliyeti artar. PostgreSQL ve MySQL'de 50 civarı pratik bir başlangıç noktasıdır, profiler ile fine-tune edilir.
GenerationType.IDENTITY kullanırken batch insert mümkün mü?
Hayır. IDENTITY stratejisi, ID'nin INSERT sonrası veritabanından dönmesini gerektirir. Hibernate her satırı tek tek göndermek zorundadır, batch tamamen devre dışı kalır. Bulk INSERT yapacaksanız SEQUENCE veya TABLE generator'a geçmek zorunludur. PostgreSQL'de SEQUENCE varsayılan tercihtir.
flush ve clear çağırmazsam ne olur?
Persistence context'te biriken entity sayısı arttıkça heap doluşur, dirty checking her flush'ta tüm entity'leri tarar ve O(n²) davranış görülür. 10.000 satırlık bir job, batch_size doğru olsa bile yavaşlar. Her batch'ten sonra flush+clear ikilisi belleği sabit tutar.
JPQL bulk update mü, entity üzerinden update mü?
Aynı koşulu binlerce satıra uygulayacaksanız JPQL UPDATE tek SQL üretir, en hızlısıdır. Ama L2 cache'i otomatik invalidate etmez, business logic'i bypass eder, listener'ları tetiklemez. Domain kuralları işin parçasıysa entity'ler üzerinden gitmek doğrudur, sadece bayrak güncelliyorsanız JPQL tercih edilir.
StatelessSession'ı ne zaman kullanmamalıyım?
Cascade gerektiren senaryolarda, audit listener'larının çalışması gereken durumlarda, dirty checking ile değişiklikleri otomatik yakalamak istediğiniz akışlarda kullanılmamalıdır. ETL, ilk veri yükleme, gece job'ları gibi domain mantığının dışında kalan bulk operasyonlar için tasarlanmıştır.
Batch ayarlarını yaptım ama hâlâ tek tek INSERT görüyorum, sorun nerede?
Üç olası neden var: birincisi IDENTITY generator kullanıyor olabilirsiniz. İkincisi JDBC sürücüsü batch'i rewrite etmiyor olabilir — PostgreSQL için reWriteBatchedInserts, MySQL için rewriteBatchedStatements URL parametresi şart. Üçüncüsü Hibernate'in show_sql çıktısı yanıltıcıdır, gerçek batch davranışını p6spy veya datasource-proxy ile gözlemlemek gerekir.
order_inserts=true performansı her zaman artırır mı?
İlişkili entity'ler farklı tablolarda peş peşe insert ediliyorsa evet, ciddi fark yaratır. Ama tek tabloya zaten sıralı kayıt yapıyorsanız etkisi marjinal olur, üstelik küçük bir sıralama maliyeti ekler. Karışık entity grafiği işliyorsanız mutlaka açın, homojen yüklemede gözle ölçüm yapın.
Spring Data JPA'nın saveAll metodu batch yapar mı?
Kendiliğinden yapmaz, sadece bir for döngüsünde save çağırır. Batch davranışı için hibernate.jdbc.batch_size ayarı, doğru ID generator ve aralıklı flush/clear gerekir. saveAll içine 10.000 entity vermek, batch_size ayarlanmamışsa 10.000 ayrı INSERT demektir — metodun adı yanıltıcıdır.


