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:
Executable
+85
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Executable
+59
@@ -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();
|
||||
}
|
||||
}
|
||||
+81
@@ -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);
|
||||
}
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+51
@@ -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");
|
||||
}
|
||||
}
|
||||
Executable
+45
@@ -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.");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user