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 org.hibernate.HibernateException; import org.hibernate.Session; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; /** * What actually happens if you mutate a {@code @NaturalId} field Hibernate believes is * immutable (the default), versus one explicitly marked {@code mutable = true}? * *

Docs: docs/06-natural-ids.md, Topic 3. */ @SpringBootTest(classes = HibernateDemoApplication.class) class NaturalIdMutabilityTest { @Autowired private EntityManagerFactory emf; @Test void mutatingAnImmutableNaturalId_throwsOnFlush() { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); ImmutableNaturalIdEntity e = new ImmutableNaturalIdEntity("CODE-A"); em.persist(e); em.getTransaction().commit(); Long id = e.getId(); em.close(); EntityManager em2 = emf.createEntityManager(); Session session = em2.unwrap(Session.class); session.getTransaction().begin(); ImmutableNaturalIdEntity managed = session.find(ImmutableNaturalIdEntity.class, id); managed.setCode("CODE-B"); // mutate the field Hibernate believes is immutable HibernateException ex = org.junit.jupiter.api.Assertions.assertThrows( HibernateException.class, () -> { session.flush(); }); System.out.println("RESULT[naturalid-immutable-mutation]: flushing a changed IMMUTABLE natural id threw: " + ex.getClass().getName() + ": " + ex.getMessage()); session.getTransaction().rollback(); em2.close(); assertThat(ex.getMessage()).containsIgnoringCase("immutable"); } @Test void mutatingAMutableNaturalId_flushesFine_andUpdatesTheColumn() { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); MutableNaturalIdEntity e = new MutableNaturalIdEntity("CODE-X"); em.persist(e); em.getTransaction().commit(); Long id = e.getId(); em.close(); EntityManager em2 = emf.createEntityManager(); em2.getTransaction().begin(); MutableNaturalIdEntity managed = em2.find(MutableNaturalIdEntity.class, id); managed.setCode("CODE-Y"); em2.getTransaction().commit(); // should NOT throw em2.close(); EntityManager em3 = emf.createEntityManager(); Object raw = em3.createNativeQuery( "select code from mutable_natural_id_entity where id = " + id).getSingleResult(); em3.close(); System.out.println("RESULT[naturalid-mutable-mutation]: flush succeeded, DB column now = " + raw); assertThat(raw).isEqualTo("CODE-Y"); } @Test void mutatingAMutableNaturalId_staleLookupByOldValue_noLongerResolves() { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); MutableNaturalIdEntity e = new MutableNaturalIdEntity("CODE-OLD"); em.persist(e); em.getTransaction().commit(); Long id = e.getId(); em.close(); EntityManager em2 = emf.createEntityManager(); em2.getTransaction().begin(); MutableNaturalIdEntity managed = em2.find(MutableNaturalIdEntity.class, id); managed.setCode("CODE-NEW"); em2.getTransaction().commit(); em2.close(); EntityManager em3 = emf.createEntityManager(); Session session = em3.unwrap(Session.class); MutableNaturalIdEntity byOld = session.bySimpleNaturalId(MutableNaturalIdEntity.class).load("CODE-OLD"); MutableNaturalIdEntity byNew = session.bySimpleNaturalId(MutableNaturalIdEntity.class).load("CODE-NEW"); em3.close(); System.out.println("RESULT[naturalid-mutable-stale-lookup]: byNaturalId(\"CODE-OLD\") = " + byOld + ", byNaturalId(\"CODE-NEW\") = " + (byNew == null ? "null" : byNew.getId())); assertThat(byOld).isNull(); assertThat(byNew).isNotNull(); assertThat(byNew.getId()).isEqualTo(id); } }