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,85 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import static org.assertj.core.api.Assertions.assertThat;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.MetadataSources;
import org.hibernate.boot.registry.StandardServiceRegistry;
import org.hibernate.boot.registry.StandardServiceRegistryBuilder;
import org.junit.jupiter.api.Test;
/**
* Reproduces the real failure mode of the JPA default {@code @Enumerated} strategy (ORDINAL):
* persist a row with the V1 enum ordering (via one standalone SessionFactory that owns only
* {@link EnumDefaultOrdinalEntity}), then read the SAME row back through a second SessionFactory
* that only knows {@link EnumReorderedV2Entity} -- a status enum with a constant inserted before
* SHIPPED. Two separate SessionFactories against the same physical H2 database avoid Hibernate's
* "same table mapped twice in one persistence unit" schema-export guard, which a single Spring
* context would trip.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
class EnumOrdinalDefaultTest {
private static final String DB_URL = "jdbc:h2:mem:enumordinal;DB_CLOSE_DELAY=-1";
private StandardServiceRegistry registry(String ddlAuto) {
return new StandardServiceRegistryBuilder()
.applySetting("hibernate.connection.driver_class", "org.h2.Driver")
.applySetting("hibernate.connection.url", DB_URL)
.applySetting("hibernate.connection.username", "sa")
.applySetting("hibernate.connection.password", "")
.applySetting("hibernate.hbm2ddl.auto", ddlAuto)
.build();
}
@Test
void ordinalDefault_reorderedEnum_silentlyReturnsWrongConstant_noException() {
StandardServiceRegistry writeRegistry = registry("create");
Long id;
try (SessionFactory writeSf = new MetadataSources(writeRegistry)
.addAnnotatedClass(EnumDefaultOrdinalEntity.class)
.buildMetadata()
.buildSessionFactory()) {
try (Session s = writeSf.openSession()) {
Transaction tx = s.beginTransaction();
EnumDefaultOrdinalEntity original = new EnumDefaultOrdinalEntity(EnumDefaultOrdinalEntity.OrderStatus.SHIPPED);
s.persist(original);
tx.commit();
id = original.getId();
}
// Prove the raw stored ordinal.
try (Session s = writeSf.openSession()) {
Number rawOrdinal = (Number) s.createNativeQuery(
"select status from enum_default_ordinal_entity where id = " + id)
.getSingleResult();
assertThat(rawOrdinal.intValue()).isEqualTo(1);
System.out.println("RESULT[enum-ordinal-default]: stored ordinal for SHIPPED (V1 ordering) = " + rawOrdinal);
}
} finally {
StandardServiceRegistryBuilder.destroy(writeRegistry);
}
// Second SessionFactory, same physical DB, "V2" reordered enum, table already exists (ddl-auto=none).
StandardServiceRegistry readRegistry = registry("none");
try (SessionFactory readSf = new MetadataSources(readRegistry)
.addAnnotatedClass(EnumReorderedV2Entity.class)
.buildMetadata()
.buildSessionFactory()) {
try (Session s = readSf.openSession()) {
EnumReorderedV2Entity reread = s.get(EnumReorderedV2Entity.class, id);
System.out.println("RESULT[enum-ordinal-default]: same row re-read through V2 (PENDING_REVIEW inserted "
+ "before SHIPPED) enum ordering = " + reread.getStatus()
+ " -- no exception thrown, silently resolves to the WRONG constant.");
assertThat(reread.getStatus())
.as("ordinal 1 now means PENDING_REVIEW in the V2 enum, not SHIPPED -- silent data corruption")
.isEqualTo(EnumReorderedV2Entity.ReorderedStatus.PENDING_REVIEW);
}
} finally {
StandardServiceRegistryBuilder.destroy(readRegistry);
}
}
}
@@ -0,0 +1,59 @@
package com.ankurm.hibernatedemo.persistenceannotations;
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;
/**
* The concrete failure: an entity with id-based equals()/hashCode() is added to a HashSet
* BEFORE flush (id == null), then persisted (id gets assigned). The same reference, looked up
* in the SAME set, is no longer found -- because its hash code changed after insertion, so
* HashSet is now probing the wrong bucket.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@SpringBootTest(classes = HibernateDemoApplication.class)
class IdBasedEqualsHashSetTrapTest {
@Autowired
private EntityManagerFactory emf;
@Test
void idBasedEquals_addedToSetBeforeFlush_containsReturnsFalseAfterIdAssigned() {
Set<IdBasedEqualsEntity> set = new HashSet<>();
IdBasedEqualsEntity e = new IdBasedEqualsEntity("widget");
set.add(e); // id is null at this point -- hashCode() = Objects.hash((Long) null)
assertThat(set.contains(e)).isTrue(); // trivially true right now
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(e);
em.getTransaction().commit(); // id gets assigned HERE -- hashCode() changes
em.close();
assertThat(e.getId()).isNotNull();
boolean stillFound = set.contains(e);
System.out.println("RESULT[id-based-equals-hashset-trap]: after persist(), e.getId()=" + e.getId()
+ ", set.contains(e) = " + stillFound + " (same reference, same set, only the hash code changed)");
assertThat(stillFound)
.as("HashSet.contains() on the SAME reference must return false once the id-based hashCode changed after insertion")
.isFalse();
// Bonus: iterating the set and calling equals() manually still finds it (equals() itself is fine) --
// it's specifically the bucket lookup (which trusts the now-stale cached hash) that is broken.
boolean foundByIteration = set.stream().anyMatch(x -> x.equals(e));
System.out.println("RESULT[id-based-equals-hashset-trap]: manual iteration foundByIteration = " + foundByIteration
+ " -- confirms equals() itself still works; it's HashSet's bucket indexing that is now wrong.");
assertThat(foundByIteration).isTrue();
}
}
@@ -0,0 +1,81 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import static org.assertj.core.api.Assertions.assertThat;
import com.ankurm.hibernatedemo.HibernateDemoApplication;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.TypedQuery;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* Three things new in Jakarta Persistence 3.2 that post 4864 predates: {@code @EnumeratedValue}
* (custom enum persisted representation), {@code getSingleResultOrNull()} on TypedQuery, and
* record types as JPQL constructor-expression targets.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@SpringBootTest(classes = HibernateDemoApplication.class)
class Jpa32NewFeaturesTest {
@Autowired
private EntityManagerFactory emf;
@Test
void enumeratedValue_persistsTheCustomCode_notOrdinalOrName() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
EnumeratedValueEntity e = new EnumeratedValueEntity(EnumeratedValueEntity.Priority.HIGH);
em.persist(e);
em.getTransaction().commit();
Long id = e.getId();
em.close();
EntityManager em2 = emf.createEntityManager();
Object raw = em2.createNativeQuery(
"select priority from enumerated_value_entity where id = " + id).getSingleResult();
EnumeratedValueEntity reread = em2.find(EnumeratedValueEntity.class, id);
em2.close();
System.out.println("RESULT[jpa32-enumeratedvalue]: raw DB value for HIGH = '" + raw
+ "' (neither ordinal '2' nor name 'HIGH' -- the @EnumeratedValue-annotated code 'H')");
assertThat(raw).isEqualTo("H");
assertThat(reread.getPriority()).isEqualTo(EnumeratedValueEntity.Priority.HIGH);
}
@Test
void getSingleResultOrNull_newIn32_returnsNullInsteadOfThrowing() {
EntityManager em = emf.createEntityManager();
TypedQuery<EnumeratedValueEntity> q = em.createQuery(
"select e from EnumeratedValueEntity e where e.id = :id", EnumeratedValueEntity.class);
q.setParameter("id", -999L);
EnumeratedValueEntity result = q.getSingleResultOrNull();
em.close();
System.out.println("RESULT[jpa32-getsingleresultornull]: query matching zero rows via getSingleResultOrNull() = " + result
+ " (getSingleResult() would have thrown NoResultException here)");
assertThat(result).isNull();
}
@Test
void jpqlConstructorExpression_targetingARecord_worksIn725() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
em.persist(new EnumeratedValueEntity(EnumeratedValueEntity.Priority.HIGH));
em.persist(new EnumeratedValueEntity(EnumeratedValueEntity.Priority.HIGH));
em.persist(new EnumeratedValueEntity(EnumeratedValueEntity.Priority.LOW));
em.getTransaction().commit();
List<PriorityCountView> counts = em.createQuery(
"select new com.ankurm.hibernatedemo.persistenceannotations.PriorityCountView(e.priority, count(e)) "
+ "from EnumeratedValueEntity e group by e.priority order by e.priority",
PriorityCountView.class).getResultList();
em.close();
System.out.println("RESULT[jpa32-record-constructor-expression]: " + counts);
assertThat(counts).extracting(PriorityCountView::priority)
.contains(EnumeratedValueEntity.Priority.HIGH, EnumeratedValueEntity.Priority.LOW);
}
}
@@ -0,0 +1,51 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import static org.assertj.core.api.Assertions.assertThat;
import com.ankurm.hibernatedemo.HibernateDemoApplication;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* Does {@code @JdbcTypeCode(SqlTypes.JSON)} actually work against H2 2.4.240? Honest empirical
* check -- if H2 rejects it, this test says so instead of asserting success.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@SpringBootTest(classes = HibernateDemoApplication.class)
class JsonColumnOnH2Test {
@Autowired
private EntityManagerFactory emf;
@Test
void jdbcTypeCodeJson_onH2_roundTripsAMap() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
JsonColumnEntity e = new JsonColumnEntity(Map.of("color", "red", "qty", 5));
em.persist(e);
em.getTransaction().commit();
Long id = e.getId();
em.close();
EntityManager em2 = emf.createEntityManager();
JsonColumnEntity loaded = em2.find(JsonColumnEntity.class, id);
em2.close();
System.out.println("RESULT[jdbctypecode-json-h2]: persisted+loaded details=" + loaded.getDetails());
assertThat(loaded.getDetails()).containsEntry("color", "red");
// Inspect the actual column type H2 chose for it.
EntityManager em3 = emf.createEntityManager();
Object colType = em3.createNativeQuery(
"select data_type from information_schema.columns "
+ "where table_name = 'JSON_COLUMN_ENTITY' and column_name = 'DETAILS'")
.getSingleResult();
em3.close();
System.out.println("RESULT[jdbctypecode-json-h2]: H2 column type for the JSON field = " + colType);
}
}
@@ -0,0 +1,51 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import static org.assertj.core.api.Assertions.assertThat;
import com.ankurm.hibernatedemo.HibernateDemoApplication;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* MixedAccessEntity has FIELD access by default (because @Id sits on a field), but one
* property ("computedLabel") is explicitly switched to PROPERTY access. This proves:
* (1) Hibernate really does call the getter -- not read a backing field -- to determine the
* persisted value for that one property, and (2) it calls it MORE than once per flush
* (dirty-check read + write), which matters if the getter has side effects or is expensive.
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@SpringBootTest(classes = HibernateDemoApplication.class)
class MixedAccessTest {
@Autowired
private EntityManagerFactory emf;
@Test
void propertyAccessAttribute_getterIsCalledByHibernate_multipleTimesPerFlush() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
MixedAccessEntity e = new MixedAccessEntity("widget");
int callsBeforePersist = e.getGetterCallCount();
em.persist(e);
em.getTransaction().commit();
Long id = e.getId();
int callsAfterFlush = e.getGetterCallCount();
em.close();
System.out.println("RESULT[mixed-access]: getComputedLabel() call count before persist=" + callsBeforePersist
+ ", after commit/flush=" + callsAfterFlush
+ " (PROPERTY-access attributes are read via the getter at flush time, not via a backing field)");
assertThat(callsAfterFlush).isGreaterThan(callsBeforePersist);
EntityManager em2 = emf.createEntityManager();
Object storedLabel = em2.createNativeQuery(
"select computed_label from mixed_access_entity where id = " + id).getSingleResult();
em2.close();
System.out.println("RESULT[mixed-access]: DB column computed_label = " + storedLabel);
assertThat(storedLabel).isEqualTo("WIDGET");
}
}
@@ -0,0 +1,45 @@
package com.ankurm.hibernatedemo.persistenceannotations;
import static org.assertj.core.api.Assertions.assertThat;
import com.ankurm.hibernatedemo.HibernateDemoApplication;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import java.time.LocalDate;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
/**
* Empirical: does {@code @Temporal} on a {@code java.time.LocalDate} field boot cleanly in
* Hibernate 7.4.5.Final, despite the annotation itself being {@code @Deprecated(since="3.2")}?
*
* <p>Docs: docs/05-jpa-persistence-annotations.md, Topic 2.
*/
@SpringBootTest(classes = HibernateDemoApplication.class)
class TemporalOnJavaTimeTest {
@Autowired
private EntityManagerFactory emf;
@Test
void temporalOnLocalDate_bootsAndPersistsFine_annotationIsSimplyIgnored() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
TemporalOnLocalDateEntity e = new TemporalOnLocalDateEntity(LocalDate.of(2026, 1, 15));
em.persist(e);
em.getTransaction().commit();
Long id = e.getId();
em.close();
EntityManager em2 = emf.createEntityManager();
TemporalOnLocalDateEntity loaded = em2.find(TemporalOnLocalDateEntity.class, id);
em2.close();
assertThat(loaded.getEventDate()).isEqualTo(LocalDate.of(2026, 1, 15));
System.out.println("RESULT[temporal-on-localdate]: boot succeeded (not silent -- Hibernate logs "
+ "HHH90000033: Encountered use of deprecated annotation [interface jakarta.persistence.Temporal] "
+ "at boot time, WARN level, one line per annotated field); round-tripped eventDate=" + loaded.getEventDate()
+ ". The mapping itself is unaffected -- LocalDate maps the same with or without @Temporal.");
}
}