İçeriğe geç

Persistence Context — Hibernate'in Hafızası

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

Önce şunu oku: JPA N+1 Problemi

30 saniyede özet

Hibernate yüklediği her nesnenin fotoğrafını tutar ve iş bitince farkı kendisi kaydeder; save() demene gerek yok. İş bittikten sonra yapılan değişiklik ise hiçbir yere yazılmaz.

Bir otel odasından çıkarken kat görevlisi neyin değiştiğini deftere yazar, sen bir şey söylemesen bile. Ama çıkış yaptıktan sonra odaya kimse bakmaz.

  1. Bayt: Kodda tek bir save() yok ama fiyat veritabanında değişmiş!

  2. Sen: Öbür metotta da save() yok ve fiyat değişmemiş.

  3. Bayt: İki metot arasındaki tek fark bir anotasyon. Neden?

  4. Bayt: Kat görevlisi odaya girerken fotoğraf çekiyor olabilir mi?

Kodda tek bir save() yok, ama veritabanında fiyat değişmiş. Başka bir metotta save() yok, fiyat değişmemiş. İki metot arasındaki tek fark bir anotasyon.

Hibernate fotoğraf çeker

persistence contextHibernate'in yönettiği entity'leri tuttuğu birinci seviye önbellek. Transaction ile birlikte açılır ve kapanır; kapandıktan sonra lazy ilişkilere dokunmak hata verir.Sözlükte gör →, Hibernate’in o an izlediği entity’lerdir. Spring’de varsayılan olarak transaction’la açılır ve commit’le kapanır.

@Transactional bir metot ürünü yükleyip fiyatını değiştiriyor. save() çağrılmıyor. UPDATE atılır mı? Cevabı göster

Atılır. Hibernate ürünü yüklerken bir anlık görüntü sakladı. Commit öncesi ikisini karşılaştırır ve farkı yazar.

Giriş fotoğrafı ile çıkıştaki oda karşılaştırılır; çıkıştan sonra kimse bakmaz.
Adım adım oku
  1. Transaction içinde ürün yüklenince Hibernate onun o anki hâlinin bir kopyasını, yani fotoğrafını saklar.
  2. Kod fiyatı 120 yapar, ama hiçbir yerde save() çağırmaz.
  3. Commit anında Hibernate nesneyi fotoğrafla karşılaştırır, farkı bulur ve UPDATE gönderir.
  4. Transaction bittikten sonra nesne detached olur. Fiyatı 150 yapsan da kimse karşılaştırmaz ve hiçbir şey yazılmaz.

Buna dirty checkingHibernate'in managed entity'lerin yüklendikleri andaki anlık görüntüsünü saklayıp flush sırasında şimdiki hâlle karşılaştırması ve farkı UPDATE olarak yazması.Sözlükte gör → denir. Managed bir entity’de değişiklik yapmak, onu kaydetmek demektir.

Kafam karıştı, daha basit anlat

Hibernate yüklediği her nesnenin fotoğrafını çeker. İş bitince nesneye tekrar bakar, fotoğraftan farklıysa değişikliği kendisi kaydeder. Senin save demene gerek kalmaz.

Hızlı kontrolBaşlangıç

JPA'daki persistence context en kısa hâliyle nedir?

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

SQL loglaması açık. Metot çağrıldığında konsolda hangi sıra görünür?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
PriceService.java
1@Transactional
2public void reprice(long id) {
3 var product = products.findById(id).orElseThrow();
4 product.setPrice(new BigDecimal("20.00"));
5 System.out.println("metot bitti");
6}
Java 21UTF-8LF

Managed ve detached

DurumNe demekDeğişiklik yazılır mı
Transientnew ile oluştu, hiç kaydedilmediHayır
ManagedAçık bir persistence context izliyorEvet, flush’ta
DetachedBir zamanlar izleniyordu, artık değilHayır

Transaction bitince her entity detached entityPersistence context'i kapanmış bir entity. Nesne hâlâ elinde, ama değişikliklerini kimse izlemez ve tembel ilişkileri artık yüklenemez.Sözlükte gör → olur. Service’ten controller’a dönen entity artık izlenmez; tembel ilişkileri de yüklenemez.

Spring Boot bunu varsayılan olarak gizler: spring.jpa.open-in-view=true isteği baştan sona açık tutar ve açılışta bir uyarı basar.

Kafam karıştı, daha basit anlat

Transaction bitince Hibernate izlemeyi bırakır. Nesne elinde kalır ama artık kimse onun fotoğrafına bakmaz, tembel ilişkileri de yüklenemez.

Hızlı kontrolOrta

Aynı `reprice` metodunda @Transactional yoksa (ve open-in-view kapalıysa) ne olur?

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

Her anda entity hangi durumda? (open-in-view kapalı)

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

Sınıflandırılmamış

Transient (yeni)

Henüz hiç kaydedilmemiş

    Managed

    Açık bir persistence context izliyor

      Detached

      Bir zamanlar izleniyordu, artık değil

        Kendin gör

        Persistence context — Hibernate neyi, ne zaman yazıyor?

        Tohum 786094

        Product#1henüz yüklenmedi

        1. Oynat ya da adımla.
        Hız
        Adım 0

        Şu an ne oldu?

        Başlıyor

        Servis ürünü yüklüyor ve fiyatını değiştiriyor. Kodda save() çağrısı yok.

        Görevler0/4

        • save() çağırmadan UPDATE attıraçık

          İpucu

          Dirty checking yalnızca managed bir entity'de ve açık bir transaction içinde çalışır.

        • Bir değişikliği sessizce kaybetaçık

          İpucu

          Aynı senaryo, ama nesneyi izleyen bir persistence context olmadan.

        • LazyInitializationException alaçık

          İpucu

          Boot'un varsayılanı bu hatayı gizliyor. Hangi ayar?

        • İki doğru isteği yanlış bir stokla bitiraçık

          İpucu

          İki istek aynı anda okursa ve UPDATE hiçbir şeyi kontrol etmezse?

        Olay günlüğü (0)

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

        1. Varsayılanla oynat. Transaction yok: fiyat değişti ama hiçbir UPDATE yok.
        2. @Transactional’ı aç. save() olmadan UPDATE atıldı.
        3. “Tembel koleksiyon” seç. Çalışıyor — ama sorgu controller’dan geliyor.
        4. open-in-view’u kapat. Aynı kod: LazyInitializationException.
        5. “İki istek” seç, sonra @Version’ı aç. 5 yerine 2: çakışma yakalandı.
        Hızlı kontrolOrta

        `LazyInitializationException: could not initialize proxy - no Session` ne anlatır?

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

        Satır satır: iki istek, tek stok

        Kim sonra yazarsa kazanır

        StockService.java
        1@Transactional
        2public void reserve(long productId, int qty) {
        şu an çalışan satır var product = products.findById(productId).orElseThrow();
        4 if (product.getStock() < qty) throw new OutOfStockException();
        5 product.setStock(product.getStock() - qty);
        6}

        Debug

        Adım 1/5

        istek A Stok 10 okundu.

        A görüyor
        = 10
        Java 21UTF-8LF3:1

        Sol/sağ ok tuşlarıyla da gezebilirsin.

        Hiçbir satır yanlış değil; yanlış olan iki doğru isteğin iç içe geçmesi. optimistic lockingKilit almadan, yazarken "okuduğumdan beri değişmedi mi?" diye kontrol etmek. JPA'da @Version alanı UPDATE'e bir sürüm koşulu ekler.Koşul tutmazsa UPDATE sıfır satır etkiler ve OptimisticLockException fırlatılır. Doğru tepki, bütün işlemi taze veriyle yeniden denemektir.Sözlükte gör → bunu yazma anında fark eder. Doğru tepki, bütün işlemi taze veriyle yeniden denemektir.

        Kafam karıştı, daha basit anlat

        İki kişi aynı rafa aynı anda bakıp “bir tane kalmış” diyor ve ikisi de alıyor. Optimistic locking, ikincisinin kasada “raf değişti” uyarısı almasıdır.

        Hızlı kontrolİleri

        Entity'ye `@Version long version` eklendi. İki transaction aynı satırı okuyup ikisi de değiştirirse ikincisinin commit'inde ne olur?

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

        Tuzaklar

        Yazan metotta readOnly = true. Flush atlanır; değişiklik hatasız kaybolur.

        LazyInitializationException’a EAGER. Hata gider, her sorgu ilişkiyi de çeker. Gerekeni o sorguda yükle: fetch join, @EntityGraph, DTO.

        Open-in-view’a güvenmek. Sorgular controller’a kayar, bağlantı istek boyunca tutulur. Yeni projede kapat.

        Bayat nesneyi yeniden kaydetmek. OptimisticLockException sonrası aynı nesneyi save etmek aynı çakışmayı tekrarlar.

        Hızlı kontrolOrta

        Controller'da `order.getItems()` LazyInitializationException veriyor. En iyi düzeltme hangisi?

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

        Ürün adı değiştirme endpoint'i hata vermiyor ama ad hiçbir zaman değişmiyor. Hangi satır sorunun kaynağı?

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

        Hatalı satıra dokun, sonra kontrol et.

        ProductService.java
        Java 21UTF-8LF

        Aşağıdaki örnek bir bankanın kart yönetim ekranından. Persistence context’in üç sonucu bir arada: save çağırmadan yazan dirty checking, iki personelin aynı limiti değiştirmesini yakalayan @Version ve çakışmayı düzgün bir 409’a çeviren hata yönetimi.

        Derinleş · Kredi kartı limiti güncelleme: iki personel, bir kart 6 dosya · ~118 satır · ilk okumada atlayabilirsin
        Proje dosyaları

        src/main/java/com/bank/card/ CardAccount.java @Version her UPDATE'e 'okuduğum sürüm hâlâ bu mu?' koşulunu ekler.

        src/main/java/com/bank/card/CardAccount.java
        @Entity
        @Table(name = "card_accounts")
        public class CardAccount {
        @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
        private Long id;
        @Version
        private long version; // UPDATE … SET version = version + 1 WHERE id = ? AND version = ?
        private BigDecimal creditLimit;
        private BigDecimal approvedMaxLimit; // set by the credit risk decision, not by the branch
        private long bonusPoints;
        @Enumerated(EnumType.STRING)
        private CardStatus status;
        public void changeLimit(BigDecimal newLimit) {
        if (status != CardStatus.ACTIVE) {
        throw new IllegalStateException("limit can only change on an active card");
        }
        if (newLimit.compareTo(approvedMaxLimit) > 0) {
        throw new LimitAboveApprovalException(id, newLimit, approvedMaxLimit);
        }
        this.creditLimit = newLimit;
        }
        public void addBonus(long points) {
        this.bonusPoints += points;
        }
        public long getVersion() { return version; }
        // other getters omitted
        }

        src/main/java/com/bank/card/ CardLimitCommands.java Save yok: yönetilen entity değişir, commit'te UPDATE gider. Sürüm istemciden gelir ve karşılaştırılır.

        src/main/java/com/bank/card/CardLimitCommands.java
        @Service
        class CardLimitCommands {
        private final CardAccountRepository cards;
        CardLimitCommands(CardAccountRepository cards) {
        this.cards = cards;
        }
        @Transactional
        public long changeLimit(long cardId, long expectedVersion, BigDecimal newLimit) {
        CardAccount card = cards.findById(cardId).orElseThrow(() -> new CardNotFoundException(cardId));
        // Two officers open the same customer; one lowers the limit after a missed payment,
        // the other raises it on the customer's request. Without this check the last
        // click wins silently and nobody knows the other decision was overwritten.
        if (card.getVersion() != expectedVersion) {
        throw new StaleCardException(cardId, expectedVersion, card.getVersion());
        }
        card.changeLimit(newLimit); // no save(): dirty checking writes the UPDATE at commit
        return card.getVersion() + 1; // the version the screen will see next
        }
        }

        src/main/java/com/bank/card/ CardLimitController.java Şube ekranı okuduğu sürümü If-Match başlığıyla geri yollar.

        src/main/java/com/bank/card/CardLimitController.java
        @RestController
        @RequestMapping("/cards/{id}/limit")
        class CardLimitController {
        private final CardLimitCommands commands;
        CardLimitController(CardLimitCommands commands) {
        this.commands = commands;
        }
        @PutMapping
        ResponseEntity<Void> change(@PathVariable long id,
        @RequestHeader(HttpHeaders.IF_MATCH) String ifMatch,
        @RequestBody @Valid NewLimit body) {
        long expected = Long.parseLong(ifMatch.replace("\"", ""));
        long next = commands.changeLimit(id, expected, body.amount());
        return ResponseEntity.noContent().eTag(String.valueOf(next)).build();
        }
        }

        src/main/java/com/bank/web/ ConcurrencyErrors.java Çakışma 409 olur; personel güncel limiti görüp kararını yeniden verir.

        src/main/java/com/bank/web/ConcurrencyErrors.java
        @RestControllerAdvice
        class ConcurrencyErrors {
        // Our own check (the officer edited a stale screen) and Hibernate's (two requests
        // raced between our check and the commit) mean the same thing to the client.
        @ExceptionHandler({StaleCardException.class, ObjectOptimisticLockingFailureException.class})
        ProblemDetail conflict(RuntimeException ex) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT,
        "Kart bilgisi siz düzenlerken değişti. Güncel limiti yükleyip yeniden karar verin.");
        problem.setTitle("Güncel olmayan veri");
        return problem;
        }
        }

        src/main/java/com/bank/card/ BonusAccrual.java İnsan müdahalesi olmayan işler için: harcama puanı eklemede çakışırsa bütün işlemi taze veriyle birkaç kez yeniden dene.

        src/main/java/com/bank/card/BonusAccrual.java
        @Service
        class BonusAccrual {
        private final TransactionTemplate tx;
        private final CardAccountRepository cards;
        BonusAccrual(PlatformTransactionManager txManager, CardAccountRepository cards) {
        this.tx = new TransactionTemplate(txManager);
        this.cards = cards;
        }
        // Called for every card purchase; two purchases a second apart can collide.
        // No one to show a 409 to: retry the WHOLE unit of work with fresh data.
        // Retrying only the flush would reuse the stale entity and fail again.
        public void accrue(long cardId, long points) {
        for (int attempt = 1; ; attempt++) {
        try {
        tx.executeWithoutResult(status -> cards.findById(cardId).orElseThrow().addBonus(points));
        return;
        } catch (ObjectOptimisticLockingFailureException ex) {
        if (attempt == 3) throw ex;
        }
        }
        }
        }

        src/main/resources/ application.yml open-in-view kapalı: lazy yükleme hataları testte görünür.

        src/main/resources/application.yml
        spring:
        jpa:
        open-in-view: false # no persistence context during view rendering or JSON writing

        Kendini sına

        Şimşek turu1/5

        Managed bir entity'deki değişiklik commit'te save() çağrılmadan yazılabilir.

        Soru 1/2İleri

        @Transactional bir metotta findById ile gelen entity değiştirildikten sonra `repository.save(entity)` çağrılıyor. Bu çağrı ne yapar?

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

        Aklında kalacak üç şey

        1. 1 Transaction içinde yüklenen bir nesneyi değiştirmek yeter; Hibernate farkı bulup UPDATE'i kendisi yazar. Transaction yoksa değişiklik sessizce kaybolur.
        2. 2 LazyInitializationException, oturum kapandıktan sonra tembel bir ilişkiye dokunulduğunu söyler. Çözüm EAGER değil, gerekeni sorguda yüklemektir.
        3. 3 Aynı satırı iki kişi okuyup yazarsa sonra yazan öncekini siler. @Version bu kayıp güncellemeyi yakalanabilir bir hataya çevirir.
        Sonraki kapı Uygulaman yaşıyorum diyor ama hiç istek alamıyor. Hangi soruyu yanlış cevapladı? Actuator ve Gözlemlenebilirlik — Sağlık, Metrik, İz · 9 dk

        5 kart sonraki derste seni bekliyor

        0/5 kart bu dersten toplandı

        Bu dersin üstüne kurulanlar

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