İçeriğe geç

ID Üretimi ve Batch Insert — 200 Satır Kaç Gidiş Dönüş?

Orta 8 dk Çok sık karşılaşılır

Önce şunu oku: Persistence Context — Hibernate'in Hafızası

30 saniyede özet

IDENTITY ile her yeni satırın numarası ancak veritabanına yazılınca belli olur, bu yüzden Hibernate satırları tek tek gönderir. Sequence ise numaraları 50'şerlik bloklarla önceden alır ve toplu yazmaya izin verir.

Paketlerini tek bir araçla göndermek istiyorsun, ama her paketin numarasını ancak merkeze ulaşınca alabiliyorsun. Araç bir türlü dolmuyor.

  1. Bayt: Binlerce satırı içe aktarmak yavaştı. Toplu yazmayı açan ayarı ekledik!

  2. Sen: Hızlandı mı?

  3. Bayt: Süre hiç değişmedi! Ayar sanki hiç yokmuş gibi.

  4. Bayt: Toplu gönderim için her paketin numarası baştan belli olmalı. Numarayı kim, ne zaman veriyor?

Binlerce satırlık içe aktarma yavaştı; ekip toplu yazmayı açan hibernate.jdbc.batch_size=50 ayarını ekledi. Süre hiç değişmedi ve sebep tek bir anotasyondu.

Id ne zaman belli olur?

Hibernate, persist edilen bir entity’yi persistence context’te id’siyle izler. Bu yüzden id’yi hemen bilmek zorundadır.

IDENTITYID'nin veritabanında INSERT sırasında üretildiği strateji (auto increment). Hibernate id'yi öğrenmek için INSERT'i hemen çalıştırır; bu yüzden batch yapamaz.Sözlükte gör →’de id’yi veritabanı INSERT sırasında üretir. Hibernate’in tek yolu INSERT’i persist anında çalıştırmaktır. Sequence’te ise id önceden alınır ve INSERT commit’e kadar bekleyebilir.

Kafam karıştı, daha basit anlat

IDENTITY’de numarayı kasa verir, bu yüzden her müşteri tek tek kasaya gitmek zorundadır. Sequence’te ise numaralar önceden bir deste hâlinde alınır.

Hızlı kontrolBaşlangıç

@GeneratedValue(strategy = GenerationType.IDENTITY) kullanan bir entity'yi persist ettiğinde INSERT ne zaman çalışır?

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

@SequenceGenerator varsayılan allocationSize değeri 50. Bunun anlamı ne?

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

Batch neden işlemiyor?

JDBC batchingAynı SQL'i farklı parametrelerle birden çok kez tek gidiş dönüşte göndermek. Hibernate'te hibernate.jdbc.batch_size ile açılır; varsayılan olarak kapalıdır.Sözlükte gör →, aynı SQL’i birçok parametreyle tek gidiş dönüşte gönderir. Hibernate’te varsayılan olarak kapalıdır.

IDENTITY kullanan 200 entity, batch_size=50 açık. Kaç INSERT çağrısı gider? Cevabı göster

200. IDENTITY insert’leri persist anında tek tek çalışır; biriktirilecek bir batch yoktur. Hibernate ayarı hata vermeden yok sayar.

Numarayı merkezden tek tek almak ya da önceden bir blok almak.
Adım adım oku
  1. IDENTITY'de id'yi veritabanı verir ve ancak satır yazılınca belli olur.
  2. Hibernate id'yi hemen bilmek zorunda olduğu için her entity'yi hemen ve ayrı ayrı yazar: toplu yazma devre dışı kalır.
  3. SEQUENCE ile veritabanı önceden bir numara bloğu verir; Hibernate numaraları kendi dağıtır.
  4. Bütün entity'ler numaralı olduğu için yazma işi biriktirilir ve tek bir toplu INSERT olarak gider.

Sequence ile batch mümkündür, ama bir tuzak daha var. Bir batch tek bir SQL şekli taşır: order, line, order, line sırası batch’i her seferinde kapatır. hibernate.order_inserts=true insert’leri tabloya göre sıralar.

Java
Java programının...
Data
...veri...
Base
...tabanına...
Connectivity
...bağlanma yolu. Java'nın veritabanıyla konuştuğu standart kapı.
Kafam karıştı, daha basit anlat

Batching, yüz mektubu tek zarfa koyup bir kez göndermektir. Ama IDENTITY her mektubu hemen yollamak zorunda olduğu için zarfta birikecek bir şey kalmaz.

Hızlı kontrolOrta

hibernate.jdbc.batch_size=50 ayarladın, ama SQL log'unda hâlâ her satır için ayrı INSERT görüyorsun. Entity'ler IDENTITY kullanıyor. Neden?

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

Her ID stratejisini, batch insert ile ilişkisine göre ayır.

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

Sınıflandırılmamış

Batch insert mümkün

Id INSERT'ten önce belli.

    Batch insert mümkün değil

    Id INSERT sırasında üretiliyor.

      Kendin gör

      200 satır kaç gidiş dönüş?

      Tohum 518743

      Oynat ya da adımla.

      Hız
      Adım 0

      Şu an ne oldu?

      IDENTITY (auto increment)

      Kod aynı: bir döngüde persist. Kaç SQL çağrısı gideceğini ID stratejisi ve iki ayar belirliyor.

      Görevler0/3

      • batch_size açıkken yine de 200 ayrı INSERT gönderaçık

        İpucu

        ID stratejisine bak.

      • Pooled sequence ve batch açık, ama her batch tek satıraçık

        İpucu

        Insert sırası.

      • 200 satırı 10'dan az gidiş dönüşle yazaçık

        İpucu

        Doğru ID stratejisi ve iki ayar.

      Olay günlüğü (0)

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

      1. Varsayılanla oynat. IDENTITY, batch açık: yine 200 INSERT.
      2. “SEQUENCE, allocationSize 1” seç. INSERT’ler batch’lendi, ama 200 sequence çağrısı var.
      3. “allocationSize 50 (pooled)” seç. Toplam 8 gidiş dönüş.
      4. order_inserts’i kapat. Batch açık, ama her batch tek satır.
      Hızlı kontrolOrta

      Pooled sequence ve batch_size=50 ayarlı. 10.000 siparişi içe aktaran iş yine de binlerce ayrı INSERT gönderiyor ve bellek sürekli büyüyor. Hatalı satırlar hangileri?

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

      Hatalı satıra dokun, sonra kontrol et.

      OrderImport.java
      Java 21UTF-8LF

      Tuzaklar

      Sequence artışı ile allocationSize uyuşmazlığı. pooled optimizerSequence'ten tek çağrıda allocationSize kadar id ayıran Hibernate stratejisi. JPA'da varsayılan allocationSize 50'dir; sequence'in artışı da buna eşit olmalıdır.Sözlükte gör → sequence’in her çağrıda 50 arttığını varsayar. Sequence INCREMENT BY 1 ise düğümler çakışan id aralıkları alabilir.

      Kendi atadığın id ve save(). Id dolu bir entity’yi Spring Data yeni saymaz ve merge çağırır; her INSERT’ten önce bir SELECT gider. Persistable ya da @Version kullan.

      Tek persistence context’te binlerce entity. Batch gönderilse bile entity’ler bellekte birikir. Döngüde her batch’te flush() ve clear() çağır.

      Hızlı kontrolOrta

      Id'yi uygulamada UUID olarak kendin atadığın bir entity'yi Spring Data'nın save() metoduyla kaydediyorsun. SQL log'unda her INSERT'ten önce bir SELECT görüyorsun. Neden?

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

      Pooled sequence kullanan bir tabloda id'ler 1, 2, 3, 51, 52, 101 diye gidiyor. Ne düşünmelisin?

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

      Aşağıdaki örnek bir bankanın gece işlediği POS takas dosyasından; her satır iki kayıtlı bir yevmiye fişine dönüşür. Toplu yazma uçtan uca: pooled sequence ile id, sequence’lerin doğru artışla oluşturulması, sıralı batch’ler, periyodik flush/clear ve id’yi uygulamada üreten bir entity için gereksiz SELECT’ten kaçış.

      Derinleş · Gece POS takas dosyası: yevmiye kayıtları, az gidiş dönüş 5 dosya · ~148 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/com/bank/ledger/ JournalEntry.java Pooled sequence: tek çağrıda 50 id. IDENTITY değil, çünkü o batch'i kapatır. Her yevmiye kaydı bir borç ve bir alacak satırı taşır.

      src/main/java/com/bank/ledger/JournalEntry.java
      // One settled card sale = one journal entry with two postings: debit the
      // merchant settlement account, credit the merchant's current account.
      @Entity
      @Table(name = "journal_entries")
      public class JournalEntry {
      @Id
      @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "journal_entries_seq")
      @SequenceGenerator(name = "journal_entries_seq", sequenceName = "journal_entries_seq", allocationSize = 50)
      private Long id;
      @OneToMany(mappedBy = "entry", cascade = CascadeType.PERSIST)
      private List<Posting> postings = new ArrayList<>();
      private String settlementRef; // the line's reference in the scheme's file
      private LocalDate valueDate;
      public static JournalEntry from(SettlementLine line, String settlementAccount) {
      var entry = new JournalEntry();
      entry.settlementRef = line.ref();
      entry.valueDate = line.valueDate();
      entry.add(Posting.debit(settlementAccount, line.netAmount()));
      entry.add(Posting.credit(line.merchantIban(), line.netAmount()));
      return entry;
      }
      private void add(Posting posting) {
      postings.add(posting);
      posting.attachTo(this);
      }
      }
      @Entity
      @Table(name = "postings")
      class Posting {
      @Id
      @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "postings_seq")
      @SequenceGenerator(name = "postings_seq", sequenceName = "postings_seq", allocationSize = 50)
      private Long id;
      @ManyToOne(fetch = FetchType.LAZY, optional = false)
      private JournalEntry entry;
      private String account;
      private BigDecimal debit;
      private BigDecimal credit;
      protected Posting() {}
      static Posting debit(String account, BigDecimal amount) {
      var p = new Posting();
      p.account = account;
      p.debit = amount;
      p.credit = BigDecimal.ZERO;
      return p;
      }
      static Posting credit(String account, BigDecimal amount) {
      var p = new Posting();
      p.account = account;
      p.debit = BigDecimal.ZERO;
      p.credit = amount;
      return p;
      }
      void attachTo(JournalEntry entry) {
      this.entry = entry;
      }
      }

      src/main/resources/db/migration/ V7__sequences.sql Sequence'ler INCREMENT BY 50 ile: allocationSize'la aynı, yoksa id aralıkları çakışabilir.

      src/main/resources/db/migration/V7__sequences.sql
      -- INCREMENT BY must equal allocationSize (50). The pooled optimizer treats each
      -- nextval as the end of a 50-id block; with INCREMENT BY 1, blocks would overlap.
      CREATE SEQUENCE journal_entries_seq START WITH 1 INCREMENT BY 50;
      CREATE SEQUENCE postings_seq START WITH 1 INCREMENT BY 50;

      src/main/java/com/bank/ledger/ SettlementImport.java İçe aktarma: persist, her 50 kayıtta flush ve clear. Yüz binlerce satırlık dosyada bile bellek sabit kalır.

      src/main/java/com/bank/ledger/SettlementImport.java
      @Service
      class SettlementImport {
      private static final int BATCH = 50; // keep in step with hibernate.jdbc.batch_size
      private final String settlementAccount;
      @PersistenceContext
      private EntityManager em;
      SettlementImport(@Value("${ledger.pos-settlement-account}") String settlementAccount) {
      this.settlementAccount = settlementAccount;
      }
      // One transaction for the whole file: the file is either fully posted or not at all,
      // so reconciliation never has to deal with half a night's settlement.
      @Transactional
      public int importFile(List<SettlementLine> lines) {
      for (int i = 0; i < lines.size(); i++) {
      em.persist(JournalEntry.from(lines.get(i), settlementAccount)); // cascade persists both postings
      if ((i + 1) % BATCH == 0) {
      em.flush(); // send the batched INSERTs
      em.clear(); // and forget the entities: memory stays flat
      }
      }
      return lines.size();
      }
      }

      src/main/resources/ application.yml Batch ayarları ve sürücüde çok satırlı INSERT'e yeniden yazma.

      src/main/resources/application.yml
      spring:
      datasource:
      # PostgreSQL driver: turn a JDBC batch into one multi-row INSERT statement.
      url: jdbc:postgresql://db:5432/ledger?reWriteBatchedInserts=true
      jpa:
      properties:
      hibernate:
      jdbc:
      batch_size: 50 # unset by default: no batching at all
      order_inserts: true # group entry/posting/entry/posting into entry…entry, posting…posting
      order_updates: true
      ledger:
      pos-settlement-account: "TR000000000000000000000001" # internal clearing account, placeholder

      src/main/java/com/bank/payment/ PaymentInstruction.java Id'yi mobil uygulama üretiyorsa Persistable: save() merge yerine persist yapar, önceden SELECT gitmez.

      src/main/java/com/bank/payment/PaymentInstruction.java
      // The mobile app generates the id before sending (a UUIDv7 keeps index inserts
      // ordered) and reuses it on retry, so a double tap cannot create two payments.
      // Spring Data would call merge() for a non-null id and SELECT before every INSERT;
      // Persistable tells it the entity is new.
      @Entity
      public class PaymentInstruction implements Persistable<UUID> {
      @Id
      private UUID id;
      private String toIban;
      private BigDecimal amount;
      @Transient
      private boolean isNew = true;
      protected PaymentInstruction() {}
      public PaymentInstruction(UUID id, String toIban, BigDecimal amount) {
      this.id = id;
      this.toIban = toIban;
      this.amount = amount;
      }
      @Override public UUID getId() { return id; }
      @Override public boolean isNew() { return isNew; }
      @PostLoad @PostPersist
      void markNotNew() {
      this.isNew = false;
      }
      }

      Kendini sına

      Şimşek turu1/5

      IDENTITY ile id, satır veritabanına yazılınca belli olur.

      Soru 1/2İleri

      Batch insert Hibernate tarafında çalışıyor (log'da 50'lik batch'ler var), ama PostgreSQL log'unda hâlâ tek tek INSERT'ler görünüyor. Ne eksik olabilir?

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

      Aklında kalacak üç şey

      1. 1 IDENTITY stratejisinde Hibernate INSERT'i hemen çalıştırır. batch_size ayarlı olsa bile bu satırlar toplu gönderilmez.
      2. 2 Pooled sequence tek seferde allocationSize kadar numara ayırır; veritabanındaki sequence'in artışı da bu değere eşit olmalıdır.
      3. 3 Toplu yazma için üç şey gerekir: önceden bilinebilen bir id, hibernate.jdbc.batch_size ve eklemeleri sıralayan order_inserts.
      Sonraki kapı Kodda önce silip sonra ekledin, ama veritabanı unique hatası verdi. SQL hangi sırayla gitti? Flush ve SQL Sırası — save() Satırında Neden Hiçbir Şey Olmuyor? · 8 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı