package com.ankurm.hibernatedemo.naturalid; import static org.assertj.core.api.Assertions.assertThat; import com.ankurm.hibernatedemo.HibernateDemoApplication; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import java.util.HashSet; import java.util.Set; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; /** * Does the "base equals()/hashCode() on the natural id, not the surrogate id" advice from * posts 4864/4865 actually hold up? Reproduces the SAME HashSet-before-flush scenario as * {@code IdBasedEqualsHashSetTrapTest} (persistenceannotations package) but with natural-id-based * equals/hashCode instead of surrogate-id-based -- this time contains() should stay TRUE. * *

Docs: docs/06-natural-ids.md, Topic 3. */ @SpringBootTest(classes = HibernateDemoApplication.class) class NaturalIdEqualsHashSetTest { @Autowired private EntityManagerFactory emf; @Test void naturalIdBasedEquals_addedToSetBeforeFlush_containsStaysTrueAfterPersist() { Set set = new HashSet<>(); NaturalIdEqualsEntity e = new NaturalIdEqualsEntity("SKU-EQ-1", "widget"); set.add(e); // sku is already set -- hashCode() is stable from construction assertThat(set.contains(e)).isTrue(); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(e); em.getTransaction().commit(); // id gets assigned, but sku (and hence hashCode) never changes em.close(); boolean stillFound = set.contains(e); System.out.println("RESULT[naturalid-equals-hashset]: after persist(), e.getId()=" + e.getId() + ", e.getSku()=" + e.getSku() + ", set.contains(e) = " + stillFound + " (equals/hashCode based on the immutable natural id, NOT the surrogate id)"); assertThat(stillFound) .as("natural-id-based equals/hashCode must survive persist() -- the advice holds up, unlike the surrogate-id version") .isTrue(); } @Test void naturalIdBasedEquals_twoTransientInstancesWithDifferentSkus_areNotEqual() { NaturalIdEqualsEntity a = new NaturalIdEqualsEntity("SKU-EQ-A", "Widget A"); NaturalIdEqualsEntity b = new NaturalIdEqualsEntity("SKU-EQ-B", "Widget B"); // Unlike surrogate-id equals (where two transient instances are BOTH "equal" because // both ids are null), natural-id equals correctly distinguishes them even before persist. System.out.println("RESULT[naturalid-equals-transient]: a.equals(b) for two DIFFERENT transient instances = " + a.equals(b) + " (both have null surrogate ids, but different natural ids)"); assertThat(a).isNotEqualTo(b); } @Test void naturalIdBasedEquals_twoTransientInstancesWithSameSku_areEqual_evenBeforePersist() { // This is the flip side worth knowing: TWO DIFFERENT transient objects representing // the "same" business entity (same sku, not yet persisted) are already equal() to // each other, well before either has a database identity. That is usually desirable, // but it means a HashSet can silently dedupe two not-yet-persisted objects. NaturalIdEqualsEntity a = new NaturalIdEqualsEntity("SKU-EQ-DUP", "First instance"); NaturalIdEqualsEntity b = new NaturalIdEqualsEntity("SKU-EQ-DUP", "Second instance, different name"); Set set = new HashSet<>(); set.add(a); boolean bWasRejectedAsADuplicate = !set.add(b); System.out.println("RESULT[naturalid-equals-transient-dup]: a.equals(b)=" + a.equals(b) + " for two transient instances sharing sku='SKU-EQ-DUP' but different names; " + "HashSet.add(b) rejected it as a duplicate = " + bWasRejectedAsADuplicate); assertThat(a).isEqualTo(b); assertThat(bWasRejectedAsADuplicate).isTrue(); } }