package com.ankurm.hibernatedemo; import static org.assertj.core.api.Assertions.assertThat; import com.ankurm.hibernatedemo.model.Book; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.OptimisticLockException; import org.hibernate.Session; import org.junit.jupiter.api.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; /** * Backs ankurm.com post 4860. Docs: docs/02-merge-vs-refresh.md. * Run with {@code ./mvnw -Dtest=OptimisticLockTest test}. * *

The specific thing this test pins down: exactly WHEN the version check fails. It would be * easy to write "merge() throws OptimisticLockException" and leave it there; what actually * happens depends on when Hibernate performs the check relative to the {@code merge()} call, the * flush, and the commit -- and that's worth being precise about rather than assumed. */ @SpringBootTest class OptimisticLockTest { private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); @Autowired private EntityManagerFactory emf; @Test void mergeOfStaleVersionedEntity_throwsOptimisticLockException_andPinsDownWhen() { // Seed. EntityManager seed = emf.createEntityManager(); seed.getTransaction().begin(); Book book = new Book("Clean Code", "Robert C. Martin"); seed.persist(book); seed.getTransaction().commit(); Long id = book.getId(); seed.close(); // Detach at version 0. EntityManager loadEm = emf.createEntityManager(); loadEm.getTransaction().begin(); Book detached = loadEm.unwrap(Session.class).get(Book.class, id); loadEm.getTransaction().commit(); loadEm.close(); assertThat(detached.getVersion()).isEqualTo(0L); // A second, independent transaction advances the row to version 1. EntityManager writer = emf.createEntityManager(); writer.getTransaction().begin(); Book row = writer.unwrap(Session.class).get(Book.class, id); row.setTitle("Clean Code (2nd Edition)"); writer.getTransaction().commit(); writer.close(); // Mutate the STILL version-0 detached instance and attempt to merge it. detached.setAuthor("Robert C. Martin (Uncle Bob)"); EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); Session session = em.unwrap(Session.class); boolean threwDuringMergeCallItself; OptimisticLockException caught = null; try { session.merge(detached); threwDuringMergeCallItself = false; } catch (OptimisticLockException e) { threwDuringMergeCallItself = true; caught = e; } if (!threwDuringMergeCallItself) { // merge() itself only queued the state transfer; the version check happens at flush. caught = org.junit.jupiter.api.Assertions.assertThrows( OptimisticLockException.class, () -> em.getTransaction().commit()); DEMO.info("OptimisticLockException surfaced at commit()/flush time, NOT from the merge() call itself."); } else { DEMO.info("OptimisticLockException surfaced directly from the merge() call."); em.getTransaction().rollback(); } assertThat(caught).isNotNull(); DEMO.info("exception: {}: {}", caught.getClass().getName(), caught.getMessage()); em.close(); // The other transaction's title change must have survived untouched. EntityManager verify = emf.createEntityManager(); verify.getTransaction().begin(); Book current = verify.unwrap(Session.class).get(Book.class, id); assertThat(current.getTitle()).isEqualTo("Clean Code (2nd Edition)"); assertThat(current.getAuthor()).isEqualTo("Robert C. Martin"); verify.getTransaction().commit(); verify.close(); } }