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:
2026-09-20 06:06:42 +00:00
committed by Claude
commit 8568c0ce6c
330 changed files with 23668 additions and 0 deletions
@@ -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();
}
}