Add Hibernate 7 batches 2-6, batch 7, and batch 8: mapping styles, JPA annotations, natural IDs, @Immutable, stored procedures, in-memory test databases, JNDI mocking, proxies, associations, temporal mapping, named queries, HQL, Criteria API, EntityManager bootstrapping, Ehcache 3 L2 cache configuration, HikariCP connection pooling, Hibernate Validator CDI integration, aggregate functions, sorting, pagination, interceptors, and Hibernate Search 8 (Hibernate 7.4.5.Final + Spring Boot 4.1.1 + JDK 25)
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.model.Book;
|
||||
import com.ankurm.hibernatedemo.model.Note;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import org.hibernate.Hibernate;
|
||||
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=MergeRefreshTest test}.
|
||||
*
|
||||
* <p>The optimistic-lock-conflict case lives in {@link OptimisticLockTest} instead, since it
|
||||
* needs its own careful narration of exactly when the check fires.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class MergeRefreshTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
private Long seedBook(String title, String status) {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Book book = new Book(title, "Test Author");
|
||||
book.setStatus(status);
|
||||
em.persist(book);
|
||||
em.getTransaction().commit();
|
||||
Long id = book.getId();
|
||||
em.close();
|
||||
return id;
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeOfDetachedInstance_returnsTheSameManagedInstanceAlreadyInSession() {
|
||||
Long id = seedBook("Managed + Detached", "DRAFT");
|
||||
|
||||
// A separate, already-detached copy of the same row (simulates "the object a controller
|
||||
// method was handed earlier").
|
||||
EntityManager scratch = emf.createEntityManager();
|
||||
scratch.getTransaction().begin();
|
||||
Book detached = scratch.unwrap(Session.class).get(Book.class, id);
|
||||
scratch.getTransaction().commit();
|
||||
scratch.close();
|
||||
detached.setAuthor("Changed On The Detached Copy");
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
|
||||
// This session already has ITS OWN managed instance for the same row before merge() is
|
||||
// ever called.
|
||||
Book managed = session.get(Book.class, id);
|
||||
|
||||
Book result = session.merge(detached);
|
||||
|
||||
assertThat(result)
|
||||
.as("merge() must return the identity-equal MANAGED instance already tracked by this session, not a new object")
|
||||
.isSameAs(managed);
|
||||
assertThat(result).isNotSameAs(detached);
|
||||
assertThat(managed.getAuthor())
|
||||
.as("the pre-existing managed instance is the one that actually receives the copied state")
|
||||
.isEqualTo("Changed On The Detached Copy");
|
||||
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeWithCascadeInitializesTheLazyCollectionAnyway() {
|
||||
Long id;
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
Book book = new Book("Lazy Collection Book", "Test Author");
|
||||
seed.persist(book);
|
||||
seed.flush();
|
||||
seed.persist(new Note("first note", book));
|
||||
seed.getTransaction().commit();
|
||||
id = book.getId();
|
||||
seed.close();
|
||||
|
||||
// Load and detach WITHOUT ever touching book.getNotes() -- the collection proxy is never
|
||||
// initialized.
|
||||
EntityManager em1 = emf.createEntityManager();
|
||||
em1.getTransaction().begin();
|
||||
Book detached = em1.unwrap(Session.class).get(Book.class, id);
|
||||
assertThat(Hibernate.isInitialized(detached.getNotes()))
|
||||
.as("sanity check: the collection must still be uninitialized going into detachment")
|
||||
.isFalse();
|
||||
em1.getTransaction().commit();
|
||||
em1.close();
|
||||
|
||||
detached.setAuthor("Edited While Detached");
|
||||
|
||||
EntityManager em2 = emf.createEntityManager();
|
||||
em2.getTransaction().begin();
|
||||
Session session = em2.unwrap(Session.class);
|
||||
|
||||
// Going in, the expectation was "merge() doesn't need to touch a collection it was
|
||||
// never asked to load." That's true ONLY when the collection has no CascadeType.MERGE.
|
||||
// Book.notes DOES cascade MERGE (see its Javadoc), and the measured result is the
|
||||
// opposite of the naive expectation: merge() initializes the collection anyway, because
|
||||
// cascading the merge to each element requires knowing what those elements are. Removing
|
||||
// cascade = CascadeType.MERGE from Book.notes and re-running this test flips the result
|
||||
// back to "stays uninitialized" -- confirmed with a throwaway probe before writing this
|
||||
// assertion. See docs/02-merge-vs-refresh.md.
|
||||
Book merged = session.merge(detached);
|
||||
|
||||
assertThat(Hibernate.isInitialized(merged.getNotes()))
|
||||
.as("merge() DOES initialize a LAZY collection when it cascades MERGE to it -- cascading requires traversal")
|
||||
.isTrue();
|
||||
assertThat(merged.getAuthor()).isEqualTo("Edited While Detached");
|
||||
|
||||
em2.getTransaction().commit();
|
||||
em2.close();
|
||||
DEMO.info("merge() initialized the LAZY notes collection because CascadeType.MERGE forces traversal of it");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshDiscardsUnflushedEditSilently_noExceptionEver() {
|
||||
Long id = seedBook("Silent Overwrite", "USER_EDIT");
|
||||
|
||||
// Simulate an admin process changing the row out from under the in-memory object.
|
||||
EntityManager admin = emf.createEntityManager();
|
||||
admin.getTransaction().begin();
|
||||
Book row = admin.unwrap(Session.class).get(Book.class, id);
|
||||
row.setStatus("ADMIN_EDIT");
|
||||
admin.getTransaction().commit();
|
||||
admin.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
Book managed = session.get(Book.class, id);
|
||||
assertThat(managed.getStatus()).isEqualTo("ADMIN_EDIT");
|
||||
|
||||
// A local, unflushed edit -- never sent to the database.
|
||||
managed.setStatus("USER_EDIT");
|
||||
assertThat(managed.getStatus()).isEqualTo("USER_EDIT");
|
||||
|
||||
session.refresh(managed);
|
||||
|
||||
assertThat(managed.getStatus())
|
||||
.as("refresh() replaces managed state with the database row -- it does not merge the two; the local edit is simply gone, no exception")
|
||||
.isEqualTo("ADMIN_EDIT");
|
||||
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user