İçeriğe geç

Toplu UPDATE — Veritabanı Değişti, Nesnen Neden Haberi Yok?

Orta 8 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

@Modifying ile yazılan toplu UPDATE doğrudan veritabanında çalışır ve bellekteki nesneleri atlar. Nesne eski değeri gösterir; kirlenirse o eski değeri geri bile yazabilir. Çaresi context'i temizlemek.

Banka her gece bir iş çalıştırıyor: bir yıldır kıpırdamayan hesaplar “uyuyan” diye işaretlenecek. Tek komut, bin hesap, iki saniye. Sonra aynı kod hesap 7’ye bakıyor ve onu hâlâ uyanık görüyor.

  1. Bayt: Bin hesabı tek sorguyla DORMANT yaptım. Süper hızlıydı.

  2. Sen: Güzel. Hesap 7 de içinde mi?

  3. Bayt: İçinde. Ama getStatus() hâlâ ACTIVE diyor!

  4. Bayt: Veritabanı bir şey söylüyor, nesne başka bir şey. Hangisine inanalım?

Toplu UPDATE kestirme yoldur

Spring Data’da @Modifying ile yazılan bir JPQL UPDATE ya da DELETE, bir bulk updateSatırları belleğe almadan tek bir UPDATE ya da DELETE ile değiştiren sorgu. Hızlıdır ama persistence context'i, @Version'ı ve entity callback'lerini atlar.Sözlükte gör → çalıştırır. Satırları belleğe hiç almaz; SQL’i doğrudan veritabanına gönderir ve kaç satırın değiştiğini döndürür.

Bu yüzden 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 →’e uğramaz. Daha önce yüklenmiş bir nesne varsa, onun alanları olduğu gibi kalır.

Kafam karıştı, daha basit anlat

Toplu UPDATE arşive doğrudan damga basar. Senin defterindeki kopyaya kimse dokunmaz.

Hızlı kontrolBaşlangıç

@Modifying @Query("update Account a set a.status = 'DORMANT' where …") çağrıldı. Hibernate ne yapar?

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

Toplu UPDATE'ten önce yüklenmiş hesap 7 nesnesi, sorgudan sonra hangi status'u gösterir?

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

Bellekteki nesne ne der?

Aynı transaction'da: findById(7) ile hesabı yükledin (ACTIVE). Sonra toplu UPDATE hesap 7'yi DORMANT yaptı. Ardından yeniden findById(7) çağırdın. Ne görürsün? Cevabı göster

ACTIVE. findById önce persistence context’e bakar. Hesap 7 orada zaten var, o yüzden SELECT bile atılmaz ve aynı eski nesne döner.

Arşive damga basıldı, defterin habersiz.
Adım adım oku
  1. findById, hesap 7'yi persistence context'e aldı: ACTIVE.
  2. Toplu UPDATE, satırları doğrudan veritabanında DORMANT yaptı.
  3. Bellekteki nesne hâlâ ACTIVE; findById da bu kopyayı döndürüyor.
  4. Context temizlenince bir sonraki find veritabanına gider ve DORMANT getirir.

Daha kötüsü de var. Eski nesnede bir alanı değiştirirsen 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 → onu commit’te yazar. Hibernate’in varsayılan UPDATE’i bütün kolonları gönderdiği için eski ACTIVE, DORMANT’ın üstüne geçer.

Çare, sorguya @Modifying(clearAutomatically = true) demek. Sorgudan sonra context boşalır ve sonraki okuma güncel satırı getirir.

Kafam karıştı, daha basit anlat

Damga basıldıktan sonra defterini at, kartı yeniden oku. Eski kopyayı arşive geri yazarsan damga silinir.

Hızlı kontrolOrta

findById(7), sonra hesap 7'yi içeren toplu UPDATE, sonra yine findById(7). İkinci çağrıda SQL log'unda ne görürsün?

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

Toplu UPDATE status'u DORMANT yaptı. Ardından eski nesnede yalnızca nickname değiştirildi. Varsayılan ayarlarla commit sonrası satırda status ne olur?

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

Kendin gör

Toplu UPDATE ve bellekteki nesne

Tohum 325345

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

Toplu UPDATE, sonra takma adı değiştir

Toplu UPDATE doğrudan veritabanında çalışır. Persistence context'teki nesneler bundan haberdar olmaz.

Görevler0/3

  • Veritabanı DORMANT derken koda ACTIVE okutaçık

    İpucu

    Varsayılan ayarlarla okuma senaryosunu dene.

  • Toplu değişikliği hiç hata almadan geri alaçık

    İpucu

    Eski nesneyi kirlet; bütün kolonlar yazılsın.

  • Takma adı değiştir, DORMANT'ı koru, bellek de güncel kalsınaçık

    İpucu

    Yalnızca yazmayı değil, okumayı da düzelten ayar hangisi?

Olay günlüğü (0)

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

  1. Varsayılanla oynat. Takma ad değişti ama satır 7 yeniden ACTIVE oldu.
  2. @DynamicUpdate’i aç. Satır DORMANT kaldı, ama Java nesnesi hâlâ ACTIVE.
  3. clearAutomatically’yi aç. Bellek de veritabanı da DORMANT.
  4. Senaryoyu okuma yap, clearAutomatically’yi kapat. SELECT hiç atılmadı, eski değer okundu.
Hızlı kontrolOrta

@Modifying(clearAutomatically = true) tam olarak ne yapar?

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

Tuzaklar

@Version artmaz. Toplu UPDATE sürüm kolonunu kendiliğinden artırmaz. Aynı satırı tutan başka bir işlem çakışmayı fark etmez; sürümü sorguda kendin artır.

@PreUpdate ve auditing çalışmaz. @LastModifiedDate gibi alanlar dolmaz, entity listener’lar tetiklenmez. Bu alanları da sorguya yaz.

Temizlik, yazılmamış değişikliği de atar. clearAutomatically bekleyen değişiklikleri siler. Garanti için flushAutomatically = true ile birlikte kullan.

Kafam karıştı, daha basit anlat

Kestirme yol hızlıdır ama sürüm numarası, tarih damgası ve dinleyiciler gibi otomatik işleri atlar. Onları sorguya kendin ekle.

Hızlı kontrolOrta

Toplu UPDATE ile bir hesabın status'unu değiştirdin. Entity'de @Version ve @LastModifiedDate var. Commit sonrası bu iki kolon için ne doğrudur?

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

Aşağıdaki örnek, bir bankanın gece çalışan “hareketsiz hesap” işinden. Toplu sorgu sürümü, tarihi ve context’i bilerek ele alıyor.

Derinleş · Hareketsiz hesaplar: toplu UPDATE'i güvenli kullanmak 4 dosya · ~83 satır · ilk okumada atlayabilirsin
Proje dosyaları

src/main/java/com/bank/account/ Account.java Hesap entity'si: optimistic locking için @Version ve auditing alanı.

src/main/java/com/bank/account/Account.java
@Entity
@Table(name = "accounts")
@EntityListeners(AuditingEntityListener.class)
class Account {
@Id
private Long id;
@Enumerated(EnumType.STRING)
private AccountStatus status;
private String nickname;
private LocalDate lastActivity;
@Version
private long version; // a bulk UPDATE will not bump this by itself
@LastModifiedDate
private Instant updatedAt; // nor fill this
protected Account() {
}
void rename(String nickname) {
this.nickname = nickname;
}
AccountStatus status() {
return status;
}
}

src/main/java/com/bank/account/ AccountRepository.java Toplu sorgu sürümü ve tarihi kendisi artırıyor; flush ve clear ikisi de açık.

src/main/java/com/bank/account/AccountRepository.java
interface AccountRepository extends JpaRepository<Account, Long> {
// flushAutomatically: pending changes reach the database before the UPDATE.
// clearAutomatically: afterwards no stale Account stays managed.
@Modifying(flushAutomatically = true, clearAutomatically = true)
@Query("""
update Account a
set a.status = com.bank.account.AccountStatus.DORMANT,
a.version = a.version + 1,
a.updatedAt = :now
where a.status = com.bank.account.AccountStatus.ACTIVE
and a.lastActivity < :cutoff
""")
int markDormant(LocalDate cutoff, Instant now);
}

src/main/java/com/bank/account/ DormancyJob.java Gece işi: toplu UPDATE'ten sonra raporu yeniden okunan nesnelerden üretiyor.

src/main/java/com/bank/account/DormancyJob.java
@Service
class DormancyJob {
private final AccountRepository accounts;
private final Clock clock;
DormancyJob(AccountRepository accounts, Clock clock) {
this.accounts = accounts;
this.clock = clock;
}
@Transactional
public int run() {
LocalDate cutoff = LocalDate.now(clock).minusYears(1);
int changed = accounts.markDormant(cutoff, clock.instant());
// The context was cleared: anything read from here on comes from the database.
return changed;
}
}

src/test/java/com/bank/account/ BulkUpdateStaleTest.java Test: clear olmadan eski nesnenin toplu değişikliği ezdiğini gösteriyor.

src/test/java/com/bank/account/BulkUpdateStaleTest.java
@DataJpaTest
@Sql("/accounts-one-active.sql") // account 7: ACTIVE, last activity in 2024
class BulkUpdateStaleTest {
@Autowired AccountRepository accounts;
@Autowired TestEntityManager em;
@Test
void afterTheBulkUpdateTheAccountIsReadFresh() {
Account seven = accounts.findById(7L).orElseThrow();
assertThat(seven.status()).isEqualTo(AccountStatus.ACTIVE);
accounts.markDormant(LocalDate.of(2026, 1, 1), Instant.parse("2026-10-02T02:00:00Z"));
// clearAutomatically detached 'seven'; this find goes to the database.
assertThat(accounts.findById(7L).orElseThrow().status()).isEqualTo(AccountStatus.DORMANT);
}
}

Kendini sına

Şimşek turu1/4

@Modifying toplu UPDATE, satırları önce belleğe yükler.

Soru 1/3Orta

clearAutomatically = true açık ama flushAutomatically kapalı. Sorgudan önce başka bir nesnede yapılmış, henüz flush edilmemiş bir değişiklik için risk nedir?

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

Aklında kalacak üç şey

  1. 1 Toplu UPDATE ve DELETE persistence context'i atlar. Daha önce yüklenen nesneler eski değerlerini taşımaya devam eder.
  2. 2 Varsayılan Hibernate UPDATE'i bütün kolonları yazar. Eski bir nesne kirlenirse toplu değişikliği hatasız ezer; clearAutomatically = true bunu önler.
  3. 3 Toplu sorgu @Version'ı artırmaz, @PreUpdate ve auditing'i çalıştırmaz. Bunlara ihtiyaç varsa ya elle yazılır ya da nesne nesne güncellenir.
Sonraki kapı Sayfada 10 sipariş var, SQL'de LIMIT yok. Veritabanından kaç satır geldi? join fetch ve Sayfalama — 10 Sipariş İçin Bütün 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.