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,330 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.immutable.AuditTrail;
|
||||
import com.ankurm.hibernatedemo.immutable.ExchangeRate;
|
||||
import com.ankurm.hibernatedemo.immutable.ExchangeRateVersioned;
|
||||
import com.ankurm.hibernatedemo.immutable.PlainRate;
|
||||
import com.ankurm.hibernatedemo.immutable.RateWithAuditTrail;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.PersistenceException;
|
||||
import java.math.BigDecimal;
|
||||
import org.hibernate.HibernateException;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.stat.Statistics;
|
||||
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 docs/07-immutable-entities.md ({@code @Immutable}, post 4866).
|
||||
* Run with {@code ./mvnw -Dtest=ImmutableEntityTest test}.
|
||||
*
|
||||
* <p>Verified on Hibernate ORM 7.4.5.Final / jakarta.persistence-api 3.2.0 / H2 2.4.240.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class ImmutableEntityTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
private Statistics stats() {
|
||||
return emf.unwrap(SessionFactory.class).getStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* THE headline claim: mutate a field on an @Immutable entity, flush inside a transaction --
|
||||
* no UPDATE is issued, AND no exception is thrown. The silence is the story.
|
||||
*/
|
||||
@Test
|
||||
void mutatingAndFlushing_immutableEntity_issuesNoUpdate_andThrowsNothing() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
ExchangeRate rate = new ExchangeRate("USD/EUR", new BigDecimal("0.9200"));
|
||||
seed.persist(rate);
|
||||
seed.getTransaction().commit();
|
||||
Long id = rate.getId();
|
||||
seed.close();
|
||||
|
||||
stats().clear();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
ExchangeRate managed = em.find(ExchangeRate.class, id);
|
||||
managed.setRate(new BigDecimal("999.9999"));
|
||||
DEMO.info("in-memory field mutated to {}, about to flush inside a transaction", managed.getRate());
|
||||
em.getTransaction().commit(); // flush + commit; must not throw
|
||||
em.close();
|
||||
|
||||
long updateCount = stats().getEntityUpdateCount();
|
||||
DEMO.info("Statistics.getEntityUpdateCount() after mutate+flush = {}", updateCount);
|
||||
assertThat(updateCount).isZero();
|
||||
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
ExchangeRate reloaded = verify.find(ExchangeRate.class, id);
|
||||
DEMO.info("reloaded from DB: {}", reloaded);
|
||||
assertThat(reloaded.getRate()).isEqualByComparingTo("0.9200");
|
||||
verify.close();
|
||||
}
|
||||
|
||||
/** @Immutable does not protect against EntityManager.remove() -- the DELETE goes through. */
|
||||
@Test
|
||||
void immutableEntity_canStillBeDeleted_viaEntityManagerRemove() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
ExchangeRate rate = new ExchangeRate("GBP/USD", new BigDecimal("1.2500"));
|
||||
seed.persist(rate);
|
||||
seed.getTransaction().commit();
|
||||
Long id = rate.getId();
|
||||
seed.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
ExchangeRate managed = em.find(ExchangeRate.class, id);
|
||||
em.remove(managed);
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
ExchangeRate gone = verify.find(ExchangeRate.class, id);
|
||||
DEMO.info("after EntityManager.remove() on an @Immutable entity, find() returns: {}", gone);
|
||||
assertThat(gone).isNull();
|
||||
verify.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Does a bulk HQL "update ... set" statement respect @Immutable? By DEFAULT in 7.4.5,
|
||||
* Hibernate refuses to even translate it -- it fails at query-compile time, before hitting
|
||||
* the database, with an InterpretationException naming the offending entity and the exact
|
||||
* setting that would allow it through.
|
||||
*/
|
||||
@Test
|
||||
void bulkHqlUpdate_onImmutableEntity_isRejectedAtTranslationTime_byDefault() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
ExchangeRate rate = new ExchangeRate("AUD/USD", new BigDecimal("0.6500"));
|
||||
seed.persist(rate);
|
||||
seed.getTransaction().commit();
|
||||
Long id = rate.getId();
|
||||
seed.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Exception caught = null;
|
||||
try {
|
||||
em.createQuery("update ExchangeRate set rate = :r where id = :id")
|
||||
.setParameter("r", new BigDecimal("42.0000"))
|
||||
.setParameter("id", id)
|
||||
.executeUpdate();
|
||||
} catch (Exception e) {
|
||||
caught = e;
|
||||
}
|
||||
em.getTransaction().rollback();
|
||||
em.close();
|
||||
|
||||
DEMO.info("bulk HQL 'update ExchangeRate set ...' on an @Immutable entity threw: {}: {}",
|
||||
caught == null ? "NOTHING" : caught.getClass().getName(),
|
||||
caught == null ? "" : caught.getMessage());
|
||||
|
||||
assertThat(caught).isInstanceOf(org.hibernate.query.sqm.InterpretationException.class);
|
||||
assertThat(caught.getMessage()).contains("attempts to update an immutable entity");
|
||||
assertThat(caught.getMessage()).contains("immutable_entity_update_query_handling_mode");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/** Does a bulk HQL "delete" statement respect @Immutable? */
|
||||
@Test
|
||||
void bulkHqlDelete_onImmutableEntity_behaviour() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
ExchangeRate rate = new ExchangeRate("NZD/USD", new BigDecimal("0.6100"));
|
||||
seed.persist(rate);
|
||||
seed.getTransaction().commit();
|
||||
Long id = rate.getId();
|
||||
seed.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
int deleted = em.createQuery("delete from ExchangeRate where id = :id")
|
||||
.setParameter("id", id)
|
||||
.executeUpdate();
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
DEMO.info("bulk HQL 'delete from ExchangeRate' executeUpdate() returned rowsAffected={}", deleted);
|
||||
assertThat(deleted).isEqualTo(1);
|
||||
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
assertThat(verify.find(ExchangeRate.class, id)).isNull();
|
||||
verify.close();
|
||||
}
|
||||
|
||||
/** Native SQL is below Hibernate's tracking entirely -- @Immutable cannot see it. */
|
||||
@Test
|
||||
void nativeSqlUpdate_onImmutableEntity_alwaysWorks() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
ExchangeRate rate = new ExchangeRate("CHF/USD", new BigDecimal("1.1000"));
|
||||
seed.persist(rate);
|
||||
seed.getTransaction().commit();
|
||||
Long id = rate.getId();
|
||||
seed.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
int rows = em.createNativeQuery("update exchange_rate set rate = 7.7 where id = " + id)
|
||||
.executeUpdate();
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
DEMO.info("native SQL UPDATE rows={}", rows);
|
||||
assertThat(rows).isEqualTo(1);
|
||||
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
ExchangeRate reloaded = verify.find(ExchangeRate.class, id);
|
||||
DEMO.info("row after native SQL update: {}", reloaded);
|
||||
assertThat(reloaded.getRate()).isEqualByComparingTo("7.7");
|
||||
verify.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Immutable on a @OneToMany collection: adding to it and flushing has historically thrown a
|
||||
* specific exception. Capture the exact class and message in Hibernate 7.4.5.
|
||||
*/
|
||||
@Test
|
||||
void addingToImmutableCollection_andFlushing_throwsSpecificException() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
RateWithAuditTrail parent = new RateWithAuditTrail("EUR/JPY");
|
||||
seed.persist(parent);
|
||||
seed.getTransaction().commit();
|
||||
Long id = parent.getId();
|
||||
seed.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
RateWithAuditTrail managed = em.find(RateWithAuditTrail.class, id);
|
||||
managed.getAuditTrails().add(new AuditTrail("added after load"));
|
||||
|
||||
Exception caught = null;
|
||||
try {
|
||||
em.getTransaction().commit();
|
||||
} catch (Exception e) {
|
||||
caught = e;
|
||||
}
|
||||
|
||||
Throwable root = caught;
|
||||
while (root != null && root.getCause() != null && root.getCause() != root) {
|
||||
root = root.getCause();
|
||||
}
|
||||
DEMO.info("adding to an @Immutable collection and flushing threw: {}",
|
||||
caught == null ? "NOTHING" : caught.getClass().getName() + ": " + caught.getMessage());
|
||||
DEMO.info("root cause class: {} message: {}",
|
||||
root == null ? "NONE" : root.getClass().getName(),
|
||||
root == null ? "" : root.getMessage());
|
||||
if (em.getTransaction().isActive()) {
|
||||
em.getTransaction().rollback();
|
||||
}
|
||||
em.close();
|
||||
|
||||
assertThat(caught).isNotNull();
|
||||
}
|
||||
|
||||
/** Does Hibernate accept @Immutable + @Version, and does the version column ever increment? */
|
||||
@Test
|
||||
void immutablePlusVersion_isAccepted_butVersionNeverIncrements() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
ExchangeRateVersioned rate = new ExchangeRateVersioned("SEK/USD", new BigDecimal("0.0950"));
|
||||
seed.persist(rate);
|
||||
seed.getTransaction().commit();
|
||||
Long id = rate.getId();
|
||||
Long initialVersion = rate.getVersion();
|
||||
seed.close();
|
||||
DEMO.info("persisted @Immutable+@Version entity, version after insert = {}", initialVersion);
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
ExchangeRateVersioned managed = em.find(ExchangeRateVersioned.class, id);
|
||||
managed.setRate(new BigDecimal("0.1234"));
|
||||
em.getTransaction().commit(); // must not throw
|
||||
em.close();
|
||||
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
ExchangeRateVersioned reloaded = verify.find(ExchangeRateVersioned.class, id);
|
||||
DEMO.info("after mutate+flush, version = {}, rate = {}", reloaded.getVersion(), reloaded.getRate());
|
||||
verify.close();
|
||||
|
||||
assertThat(reloaded.getVersion()).isEqualTo(initialVersion);
|
||||
assertThat(reloaded.getRate()).isEqualByComparingTo("0.0950");
|
||||
}
|
||||
|
||||
/**
|
||||
* Session.setReadOnly(entity, true) on a PLAIN (non-@Immutable) entity: same silent-no-UPDATE
|
||||
* outcome as @Immutable, but it is a per-entity-instance runtime toggle rather than a
|
||||
* class-wide, DDL-agnostic mapping annotation.
|
||||
*/
|
||||
@Test
|
||||
void sessionSetReadOnlyOnPlainEntity_alsoSuppressesUpdate_perInstance() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
PlainRate p = new PlainRate("PLN/USD", new BigDecimal("4.0"));
|
||||
seed.persist(p);
|
||||
seed.getTransaction().commit();
|
||||
Long id = p.getId();
|
||||
seed.close();
|
||||
|
||||
stats().clear();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
Session session = em.unwrap(Session.class);
|
||||
em.getTransaction().begin();
|
||||
PlainRate managed = session.find(PlainRate.class, id);
|
||||
session.setReadOnly(managed, true);
|
||||
managed.setRate(new BigDecimal("999"));
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
|
||||
long updateCount = stats().getEntityUpdateCount();
|
||||
DEMO.info("Session.setReadOnly(entity,true) then mutate+flush -> entityUpdateCount = {}", updateCount);
|
||||
assertThat(updateCount).isZero();
|
||||
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
PlainRate reloaded = verify.find(PlainRate.class, id);
|
||||
assertThat(reloaded.getRate()).isEqualByComparingTo("4.0");
|
||||
verify.close();
|
||||
}
|
||||
|
||||
/** Session.setDefaultReadOnly(true) applies the read-only flag to every entity loaded afterwards. */
|
||||
@Test
|
||||
void sessionSetDefaultReadOnly_appliesToAllSubsequentLoads() {
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
PlainRate p = new PlainRate("DKK/USD", new BigDecimal("6.9"));
|
||||
seed.persist(p);
|
||||
seed.getTransaction().commit();
|
||||
Long id = p.getId();
|
||||
seed.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
Session session = em.unwrap(Session.class);
|
||||
session.setDefaultReadOnly(true);
|
||||
em.getTransaction().begin();
|
||||
PlainRate managed = session.find(PlainRate.class, id);
|
||||
managed.setRate(new BigDecimal("111"));
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
PlainRate reloaded = verify.find(PlainRate.class, id);
|
||||
DEMO.info("after setDefaultReadOnly(true) + mutate + flush, rate = {}", reloaded.getRate());
|
||||
assertThat(reloaded.getRate()).isEqualByComparingTo("6.9");
|
||||
verify.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user