Run with {@code ./mvnw -Dtest=HqlQueryTest test}. Every assertion here was first observed by
* running the same query and reading the log, then pinned down as an assertion.
*/
@SpringBootTest
class HqlQueryTest {
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
@Autowired
private EntityManagerFactory emf;
private Long engineeringId;
private Long marketingId;
@BeforeEach
void seed() {
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
// This method-scoped H2 database (jdbc:h2:mem:hibernate-demo) is shared across every
// test in the class run, not reset between tests, so each test must clear its own
// tables first -- otherwise counts creep upward test by test.
em.createQuery("DELETE FROM Employee").executeUpdate();
em.createQuery("DELETE FROM QueryDept").executeUpdate();
Department engineering = new Department("Engineering");
Department marketing = new Department("Marketing");
em.persist(engineering);
em.persist(marketing);
em.persist(new Employee("Ada", "Byron", 95_000.0, "ACTIVE", LocalDate.of(2019, 3, 1), engineering));
em.persist(new Employee("Grace", "Hopper", 98_000.0, "ACTIVE", LocalDate.of(2018, 6, 15), engineering));
em.persist(new Employee("Linus", "Torvalds", 92_000.0, "INACTIVE", LocalDate.of(2015, 1, 10), engineering));
em.persist(new Employee("Margaret", "Hamilton", 72_000.0, "ACTIVE", LocalDate.of(2020, 9, 1), marketing));
em.persist(new Employee("Katherine", "Johnson", 71_000.0, "ACTIVE", LocalDate.of(2021, 2, 20), marketing));
em.getTransaction().commit();
engineeringId = engineering.getId();
marketingId = marketing.getId();
em.close();
}
private Statistics stats() {
return emf.unwrap(SessionFactory.class).getStatistics();
}
@Test
void selectAll_returnsEveryRow() {
EntityManager em = emf.createEntityManager();
List all = em.createQuery("FROM Employee", Employee.class).getResultList();
assertThat(all).hasSize(5);
em.close();
}
@Test
void whereWithNamedParameter_filtersCorrectly() {
EntityManager em = emf.createEntityManager();
List active = em.createQuery(
"SELECT e FROM Employee e WHERE e.status = :status", Employee.class)
.setParameter("status", "ACTIVE")
.getResultList();
assertThat(active).hasSize(4).allMatch(e -> "ACTIVE".equals(e.getStatus()));
DEMO.info("whereWithNamedParameter: {} active employees", active.size());
em.close();
}
@Test
void columnNameInsteadOfFieldName_failsAtQueryTimeNotSilently() {
// The classic HQL pitfall: writing the database column name (first_name) instead of
// the entity field name (firstName). This is NOT a silent wrong-result bug -- HQL
// resolves against the entity model, so an unknown path fails loudly before any SQL
// is sent.
EntityManager em = emf.createEntityManager();
assertThatThrownBy(() -> em.createQuery("SELECT e FROM Employee e WHERE e.first_name = 'Ada'", Employee.class))
.isInstanceOfAny(IllegalArgumentException.class, SemanticException.class)
.satisfies(ex -> DEMO.info("columnNameInsteadOfFieldName: {}: {}", ex.getClass().getSimpleName(), ex.getMessage()));
em.close();
}
@Test
void joinWithoutFetch_causesOneQueryPerDepartmentAccess() {
EntityManager em = emf.createEntityManager();
stats().clear();
List withDept = em.createQuery(
"SELECT e FROM Employee e JOIN e.department d WHERE d.name = :deptName", Employee.class)
.setParameter("deptName", "Engineering")
.getResultList();
long afterQuery = stats().getPrepareStatementCount();
assertThat(afterQuery).as("plain JOIN issues one SELECT for the employees").isEqualTo(1);
// Touching the association now triggers a SEPARATE select per distinct department --
// this is the N+1 that JOIN alone (without FETCH) does not prevent.
withDept.forEach(e -> e.getDepartment().getName());
long afterTouch = stats().getPrepareStatementCount();
assertThat(afterTouch).as("touching the lazy association after a plain JOIN fires extra SELECTs")
.isGreaterThan(afterQuery);
DEMO.info("joinWithoutFetch: {} statements for the query, {} after touching department", afterQuery, afterTouch);
em.close();
}
@Test
void joinFetch_loadsAssociationInOneQuery() {
EntityManager em = emf.createEntityManager();
stats().clear();
List withFetch = em.createQuery(
"SELECT e FROM Employee e LEFT JOIN FETCH e.department", Employee.class)
.getResultList();
withFetch.forEach(e -> e.getDepartment().getName());
long total = stats().getPrepareStatementCount();
assertThat(total).as("JOIN FETCH loads employees AND departments in exactly one SELECT, even after touching department")
.isEqualTo(1);
DEMO.info("joinFetch: {} statement total, {} rows", total, withFetch.size());
em.close();
}
@Test
void aggregateCount_matchesSeedSize() {
EntityManager em = emf.createEntityManager();
Long count = em.createQuery("SELECT COUNT(e) FROM Employee e", Long.class).getSingleResult();
assertThat(count).isEqualTo(5L);
DEMO.info("aggregateCount: {}", count);
em.close();
}
@Test
void avgSalaryGroupByDepartment_matchesHandComputedAverages() {
EntityManager em = emf.createEntityManager();
List