İçeriğe geç

Entity equals ve hashCode — Aynı Sipariş mi?

Orta 8 dk Sık karşılaşılır

Önce şunu oku: Cascade ve İlişkinin Sahibi — Kalem Gerçekten Eklendi mi? , HashMap, equals ve hashCode

30 saniyede özet

Bir entity'nin id'si kaydedilince değişir, alanları değişir, bazen sahte bir vekil nesne (proxy) olarak gelir. equals ve hashCode bunların hepsinde aynı cevabı vermeli: ya hiç değişmeyen bir anahtar, ya da id + sabit hashCode.

Sipariş formunda iki yeni kalem eklendi, kaydedildikten sonra yalnızca biri göründü. Başka bir ekranda da kaydedilen bir kalem Set’ten silinemedi. İkisinin sebebi aynı equals metoduydu.

  1. Bayt: Formda iki yeni kalem ekledim, kaydedince yalnızca biri göründü!

  2. Sen: Öbürü nereye gitti?

  3. Bayt: Üstelik kaydettiğim bir kalemi Set'ten silemiyorum. İkisi de aynı equals yüzündenmiş.

  4. Bayt: Bir misafiri oda numarasıyla tanımak, odası yokken ne demek?

Bir entity’nin dört hâli var

Tek bir persistence context içinde Hibernate her satır için tek nesne verir; orada nesne kimliği yeterlidir. Sorun şu dört anda başlar:

  1. Yeni nesnenin id’si null’dır, persist’te atanır.
  2. Alanları değişir.
  3. Aynı satır farklı transaction’larda farklı nesnelerdir.
  4. Tembel ilişki bir Hibernate proxyTembel yükleme için entity sınıfından türetilmiş yer tutucu. Alanları ilk erişime kadar boştur ve getClass() gerçek sınıfı değil proxy sınıfını döndürür.Sözlükte gör →’sidir; sınıfı farklıdır, alanları boştur.
Kafam karıştı, daha basit anlat

Aynı satırı iki ayrı zamanda yüklersen iki ayrı nesne alırsın. Java onların aynı kayıt olduğunu, sen equals ile söylemedikçe bilemez.

Hızlı kontrolBaşlangıç

Aynı transaction içinde em.find(Order.class, 42) iki kez çağrıldı. İki sonuç için a == b ne döner?

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

equals override edilmemiş. Bir istek, müşterinin adreslerini (detached) bir Set'e koyuyor, sonra aynı adresleri başka bir sorgudan yükleyip Set'e ekliyor. Set'in boyutu ne olur?

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

hashCode değişirse ne olur?

hashCode Objects.hash(id). Yeni sipariş HashSet'e eklendi, sonra persist edildi ve id 42 oldu. set.contains(siparis) ne döner? Cevabı göster

false. HashSet nesneyi eklendiği andaki hash’e göre bir bucket’a koydu. id atanınca hash değişti; nesne Set’in içinde, ama Set onu başka bir bucket’ta arıyor.

Oda numarası sonradan verilir; pasaport ilk günden bellidir.
Adım adım oku
  1. İki yeni kalemin henüz id'si yok. Id'ye göre yazılmış equals ikisini aynı sayar ve kümede yalnızca biri kalır.
  2. Yeni kalem, id'si null iken hesaplanan hashCode'a göre kümenin oda yok rafına konur.
  3. Persist edilince id 42 olur; contains artık 42 rafına bakar ve kalemi bulamaz.
  4. Oluştururken verilen ve hiç değişmeyen bir kimlik, pasaport numarası gibi, iki sorunu da önler.

İki güvenli yol var. Değişmez bir natural idEntity'yi iş dünyasında tanımlayan, değişmeyen anahtar: ISBN, sipariş numarası. Veritabanı id'sinden farklı olarak nesne oluşturulurken bellidir.Sözlükte gör → varsa (sipariş numarası, ISBN) equals ve hashCode yalnızca ona bakar. Yoksa equals id’ye bakar, id null iken nesne yalnızca kendine eşittir ve hashCode sınıfa bağlı sabittir.

Order.java
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Order other)) return false;
return getId() != null && getId().equals(other.getId());
}
public int hashCode() {
return Order.class.hashCode();
}
Kafam karıştı, daha basit anlat

Yeni nesnenin henüz id’si yoktur, kaydedince id gelir. hashCode id’ye bakıyorsa nesnenin rafı değişir, Set de onu eski rafta bulamaz.

Hızlı kontrolOrta

equals ve hashCode yalnızca id'ye bakıyor: Objects.hash(id). Yeni bir sipariş HashSet'e eklendi, sonra persist edildi. set.contains(order) ne döner?

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

Kitap entity'si için equals/hashCode'a en uygun alan hangisi?

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

Kendin gör

Aynı sipariş mi?

Tohum 320993

Oynat ya da adımla.

Hız
Adım 0

Şu an ne oldu?

id ile, IDE üretimi (getClass + Objects.hash(id))

Bir entity'nin hayatında id'si değişir, alanları değişir, farklı transaction'larda farklı nesneler olur ve bazen bir proxy'dir. equals hepsinde tutarlı mı?

Görevler0/3

  • Bir siparişi HashSet'in içinde kaybetaçık

    İpucu

    id'ye bakan, IDE'nin ürettiği equals.

  • equals çağrısıyla LazyInitializationException alaçık

    İpucu

    Her alanı karşılaştır.

  • Dört kontrolün hepsini geçaçık

    İpucu

    İki doğru yol var.

Olay günlüğü (0)

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

  1. Varsayılanla oynat. IDE’nin ürettiği id tabanlı equals: Set’te kayboldu, proxy’yi reddetti.
  2. “Bütün alanlar” seç. Alan değişince kayboldu, detached nesnede exception.
  3. “id ile, instanceof + sabit hashCode” seç. Dört kontrolün dördü.
  4. “Doğal anahtar” seç. Yine dördü.
Hızlı kontrolOrta

Bu equals, em.getReference ile alınan proxy'yi aynı id'li entity'ye eşit saymı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.

Order.java
Java 21UTF-8LF

Her equals/hashCode uygulamasını ayır.

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

Sınıflandırılmamış

Entity için güvenli

Persist, değişiklik, detached ve proxy'de tutarlı.

    Sorunlu

    En az bir durumda yanlış ya da exception.

      Tuzaklar

      Lombok @Data. Bütün alanları, ilişkiler dahil, equals, hashCode ve toString’e koyar. Karşılıklı ilişkide sonsuz döngü, transaction dışında LazyInitializationException üretir.

      id null iken true. İki yeni nesne birbirine eşit sayılır ve Set ikincisini sessizce atar.

      getClass() karşılaştırması. Proxy entity’nin alt sınıfıdır; getClass() farklı döner ve aynı satır kendisine eşit sayılmaz.

      Kafam karıştı, daha basit anlat

      Değişmeyen bir numara (sipariş numarası gibi) varsa eşitliği ona bağla. Yoksa id’ye bak, ve hashCode’u hiç değişmeyecek bir değer yap.

      Hızlı kontrolİleri

      Bir @OneToMany Set<OrderLine> lines koleksiyonunda, persist edilmemiş iki yeni kalem ekleniyor ama Set'te yalnızca biri kalıyor. equals id'ye bakıyor ve id null iken true dönüyor. Neden?

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

      Aşağıdaki örnek bir bankadan ve iki güvenli yolu da gösteriyor: doğal anahtarı (IBAN) olan Account ve olmayan Transfer. Test, dersteki dört kontrolü gerçek Hibernate ile yapıyor.

      Derinleş · Havale ve hesap: iki güvenli equals, dört kontrollük test 4 dosya · ~125 satır · ilk okumada atlayabilirsin
      Proje dosyaları

      src/main/java/com/bank/transfer/ Transfer.java Doğal anahtar yok (bir havale talimatı): id + instanceof + getter + sabit hashCode. id null iken yalnızca kendine eşit.

      src/main/java/com/bank/transfer/Transfer.java
      @Entity
      @Table(name = "transfers")
      public class Transfer {
      @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
      private Long id;
      @Enumerated(EnumType.STRING)
      private TransferStatus status = TransferStatus.PENDING;
      public Long getId() { return id; }
      public void markSent() { status = TransferStatus.SENT; }
      @Override
      public boolean equals(Object o) {
      if (this == o) return true;
      // instanceof accepts Hibernate's proxy subclass; getClass() would not.
      if (!(o instanceof Transfer other)) return false;
      // Getter, not field: a proxy's fields are empty, getId() is not.
      // A new transfer (id == null) equals only itself.
      return getId() != null && getId().equals(other.getId());
      }
      @Override
      public int hashCode() {
      // Constant for the class: the same before and after persist assigns the id,
      // and the same for a proxy (getClass().hashCode() would differ).
      return Transfer.class.hashCode();
      }
      }

      src/main/java/com/bank/account/ Account.java Doğal anahtar var (bir banka hesabı): IBAN değişmez ve benzersiz; equals yalnızca ona bakar.

      src/main/java/com/bank/account/Account.java
      @Entity
      public class Account {
      @Id @GeneratedValue(strategy = GenerationType.SEQUENCE)
      private Long id;
      @NaturalId // unique, immutable; Hibernate adds a unique constraint
      @Column(nullable = false, updatable = false, length = 34)
      private String iban;
      private String nickname; // mutable ("Maaş hesabım"): never part of equals
      private BigDecimal balance; // mutable: never part of equals
      protected Account() {}
      public Account(String iban, String nickname) {
      this.iban = Objects.requireNonNull(iban);
      this.nickname = nickname;
      this.balance = BigDecimal.ZERO;
      }
      public String getIban() { return iban; }
      public void rename(String nickname) { this.nickname = nickname; }
      @Override
      public boolean equals(Object o) {
      if (this == o) return true;
      if (!(o instanceof Account other)) return false;
      return getIban().equals(other.getIban());
      }
      @Override
      public int hashCode() {
      return getIban().hashCode(); // known from construction, never changes
      }
      }

      src/main/java/com/bank/account/ AccountLookup.java Doğal anahtarla yükleme: Hibernate'in natural-id API'si, id'ye çeviren ek bir sorgu yazmadan.

      src/main/java/com/bank/account/AccountLookup.java
      @Repository
      class AccountLookup {
      @PersistenceContext
      private EntityManager em;
      Optional<Account> byIban(String iban) {
      return em.unwrap(Session.class)
      .bySimpleNaturalId(Account.class)
      .loadOptional(iban);
      }
      }

      src/test/java/com/bank/transfer/ TransferEqualityTest.java Dersteki dört kontrol: Set'e ekle ve persist et, alan değiştir, iki transaction, proxy.

      src/test/java/com/bank/transfer/TransferEqualityTest.java
      @DataJpaTest
      class TransferEqualityTest {
      @Autowired TestEntityManager em;
      @Test
      void staysInTheSetAcrossPersist() {
      Set<Transfer> set = new HashSet<>();
      Transfer transfer = new Transfer();
      set.add(transfer); // id == null
      em.persistAndFlush(transfer); // id assigned
      assertThat(set).contains(transfer);
      }
      @Test
      void staysInTheSetWhenAFieldChanges() {
      Transfer transfer = em.persistAndFlush(new Transfer());
      Set<Transfer> set = new HashSet<>(Set.of(transfer));
      transfer.markSent();
      assertThat(set).contains(transfer);
      }
      @Test
      void twoLoadsOfTheSameRowAreEqual() {
      Long id = em.persistAndGetId(new Transfer(), Long.class);
      em.flush();
      em.clear();
      Transfer first = em.find(Transfer.class, id);
      em.clear(); // a second persistence context, in effect
      Transfer second = em.find(Transfer.class, id);
      assertThat(first).isNotSameAs(second).isEqualTo(second);
      }
      @Test
      void theEntityEqualsAProxyOfTheSameRow() {
      Long id = em.persistAndGetId(new Transfer(), Long.class);
      em.flush();
      em.clear();
      Transfer proxy = em.getEntityManager().getReference(Transfer.class, id); // never initialised
      em.clear();
      Transfer loaded = em.find(Transfer.class, id);
      // loaded.equals(proxy) only calls proxy.getId(), which Hibernate answers from
      // the proxy itself. (Calling an overridden method like hashCode() on a detached,
      // uninitialised proxy would try to load it and fail.)
      assertThat(loaded).isEqualTo(proxy);
      }
      }

      Kendini sına

      Şimşek turu1/5

      Yeni bir entity'nin id'si persist edilmeden önce genellikle null'dır.

      Soru 1/2Orta

      Bir entity'ye Lombok @Data eklendi. Order'da tembel bir lines koleksiyonu, OrderLine'da da order alanı var. İlk ne görürsün?

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

      Aklında kalacak üç şey

      1. 1 Objects.hash(id) kaydetme anında değişir; kaydetmeden önce HashSet'e konan entity sonra bulunamaz.
      2. 2 Güvenli iki yol var: hiç değişmeyen doğal bir anahtar, ya da id'ye bakan equals ile sabit bir hashCode.
      3. 3 Proxy'lerle çalışmak için getClass() yerine instanceof, alan yerine getter kullanılır. İlişkiler ve değişen alanlar equals'a girmez.
      Sonraki kapı Bir güncellemeden sonra bazı sunucular hâlâ eski fiyatı gösteriyor. Veri nerede saklanıyordu? İkinci Seviye Cache — Hangi Cache, Hangi Soruya Cevap Verir? · 9 dk

      5 kart sonraki derste seni bekliyor

      0/5 kart bu dersten toplandı