package com.ankurm.hibernatedemo.namedquery; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.NoResultException; import jakarta.persistence.Query; import java.util.List; import org.hibernate.SessionFactory; import org.hibernate.stat.Statistics; import org.junit.jupiter.api.BeforeEach; 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; import org.springframework.test.context.TestPropertySource; /** * Backs ankurm.com post 4877, docs/14-named-queries.md. Covers: jakarta @NamedQuery execution, * org.hibernate.annotations.NamedQuery's cacheable extra actually taking effect, * @NamedNativeQuery + @SqlResultSetMapping DTO projection, record constructor-result (JPA 3.2), * and getSingleResultOrNull() vs getSingleResult(). */ @SpringBootTest(properties = { "spring.jpa.properties.hibernate.cache.use_second_level_cache=true", "spring.jpa.properties.hibernate.cache.use_query_cache=true", "spring.jpa.properties.hibernate.cache.region.factory_class=jcache", "spring.jpa.properties.hibernate.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider" }) class NamedQueryExecutionTest { private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); @Autowired private EntityManagerFactory emf; @BeforeEach void cleanTables() { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.createQuery("DELETE FROM NqEmployee").executeUpdate(); em.createQuery("DELETE FROM HibernateExtraEmployee").executeUpdate(); em.getTransaction().commit(); em.close(); } private Statistics stats() { return emf.unwrap(SessionFactory.class).getStatistics(); } @Test void jakartaNamedQuery_executesAndReturnsExpectedRows() { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(new NqEmployee("Ankur", "ACTIVE")); em.persist(new NqEmployee("Priya", "ACTIVE")); em.getTransaction().commit(); List found = em.createNamedQuery("Employee.findByName", NqEmployee.class) .setParameter("name", "Ankur") .getResultList(); DEMO.info("Employee.findByName(Ankur): {} rows", found.size()); assertThat(found).hasSize(1); em.close(); } @Test void hibernateNamedQuery_cacheableExtra_actuallyMarksTheQueryCacheable() { // org.hibernate.annotations.NamedQuery offers `cacheable` -- jakarta.persistence.NamedQuery // does not. Verify it actually takes effect using Statistics' query-cache put count. EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(new HibernateExtraEmployee("A")); em.persist(new HibernateExtraEmployee("B")); em.getTransaction().commit(); em.close(); stats().clear(); EntityManager em1 = emf.createEntityManager(); List first = em1.createNamedQuery("HibernateExtraEmployee.cacheableFindAll", HibernateExtraEmployee.class) .getResultList(); long putsAfterFirst = stats().getQueryCachePutCount(); em1.close(); EntityManager em2 = emf.createEntityManager(); List second = em2.createNamedQuery("HibernateExtraEmployee.cacheableFindAll", HibernateExtraEmployee.class) .getResultList(); long hitsAfterSecond = stats().getQueryCacheHitCount(); em2.close(); DEMO.info("cacheable=true named query: puts after 1st run = {}, cache hits after 2nd run = {}", putsAfterFirst, hitsAfterSecond); assertThat(first).hasSize(2); assertThat(second).hasSize(2); assertThat(putsAfterFirst).as("cacheable=true on org.hibernate.annotations.NamedQuery must populate the query cache").isGreaterThan(0); assertThat(hitsAfterSecond).as("second identical call must hit the query cache").isGreaterThan(0); } @Test void namedNativeQuery_withSqlResultSetMapping_projectsToDto() { EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(new NqEmployee("Native1", "ACTIVE")); em.persist(new NqEmployee("Native2", "INACTIVE")); em.getTransaction().commit(); @SuppressWarnings("unchecked") List dtos = (List) (List) em.createNamedQuery("Employee.byNativeDto") .setParameter("status", "ACTIVE") .getResultList(); DEMO.info("Employee.byNativeDto(ACTIVE): {}", dtos); assertThat(dtos).hasSize(1); assertThat(dtos.get(0).getFirstName()).isEqualTo("Native1"); em.close(); } @Test void jpql31ConstructorExpression_worksWithARecordAsTheTarget() { // Jakarta Persistence 3.2 alternative to @SqlResultSetMapping: a plain JPQL constructor // expression targeting a java `record`. EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(new NqEmployee("RecordTest", "ACTIVE")); em.getTransaction().commit(); List records = em.createQuery( "SELECT NEW com.ankurm.hibernatedemo.namedquery.EmployeeRecordDto(e.id, e.firstName) FROM NqEmployee e WHERE e.firstName = :name", EmployeeRecordDto.class) .setParameter("name", "RecordTest") .getResultList(); DEMO.info("JPQL constructor expression into a record: {}", records); assertThat(records).hasSize(1); assertThat(records.get(0).firstName()).isEqualTo("RecordTest"); em.close(); } @Test void getSingleResultOrNull_vs_getSingleResult_onNoRows() { EntityManager em = emf.createEntityManager(); Object viaOrNull = em.createQuery("SELECT e FROM NqEmployee e WHERE e.firstName = :n") .setParameter("n", "NoSuchPersonAtAll") .getSingleResultOrNull(); DEMO.info("getSingleResultOrNull() on zero rows returned: {}", viaOrNull); assertThat(viaOrNull).isNull(); Query q = em.createQuery("SELECT e FROM NqEmployee e WHERE e.firstName = :n") .setParameter("n", "NoSuchPersonAtAll"); NoResultException ex = assertThrows(NoResultException.class, q::getSingleResult); DEMO.info("getSingleResult() on zero rows threw: {}: {}", ex.getClass().getName(), ex.getMessage()); em.close(); } }