test(document): add query-count assertions for findAll + findById entity graphs

Adds Hibernate statistics to the test config and two new tests in
DocumentRepositoryTest:
- findAll_withSpecAndPageable asserts ≤5 statements for 10 documents
  (currently RED: EAGER @ManyToMany generates 31 secondary SELECTs)
- findById regression guard verifies collections load in ≤2 statements

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-05-18 16:43:36 +02:00
parent 193a4d6ee6
commit ff792e6625
2 changed files with 74 additions and 0 deletions

View File

@@ -1,5 +1,9 @@
package org.raddatz.familienarchiv.document;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.junit.jupiter.api.Test;
import org.raddatz.familienarchiv.PostgresContainerConfig;
import org.raddatz.familienarchiv.config.FlywayConfig;
@@ -21,6 +25,7 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@@ -55,6 +60,12 @@ class DocumentRepositoryTest {
@Autowired
private TranscriptionBlockRepository transcriptionBlockRepository;
@Autowired
private EntityManagerFactory entityManagerFactory;
@Autowired
private EntityManager entityManager;
// ─── save and findById ────────────────────────────────────────────────────
@Test
@@ -490,6 +501,65 @@ class DocumentRepositoryTest {
assertThat(ids).containsExactlyInAnyOrder(grandparent.getId(), parent2.getId(), child2.getId());
}
// ─── query-count — entity-graph assertions ────────────────────────────────
@Test
void findAll_withSpecAndPageable_loadsDocumentsInAtMostFiveStatements() {
Person sender = personRepository.save(Person.builder().firstName("Hans").lastName("QcSender").build());
Person receiver = personRepository.save(Person.builder().firstName("Anna").lastName("QcReceiver").build());
Tag tag = tagRepository.save(Tag.builder().name("QcTag").build());
for (int i = 0; i < 10; i++) {
documentRepository.save(Document.builder()
.title("QcDoc " + i).originalFilename("qcdoc" + i + ".pdf")
.status(DocumentStatus.UPLOADED)
.sender(sender)
.receivers(new HashSet<>(Set.of(receiver)))
.tags(new HashSet<>(Set.of(tag)))
.build());
}
entityManager.flush();
entityManager.clear();
Statistics stats = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
stats.setStatisticsEnabled(true);
stats.clear();
Specification<Document> allDocs = (root, query, cb) -> null;
documentRepository.findAll(allDocs, PageRequest.of(0, 10));
assertThat(stats.getPrepareStatementCount())
.as("@EntityGraph(Document.list) must load 10 docs in ≤5 statements, not N+1")
.isLessThanOrEqualTo(5);
}
@Test
void findById_loadsSenderReceiversAndTagsInAtMostTwoStatements() {
Person sender = personRepository.save(Person.builder().firstName("Max").lastName("FbSender").build());
Set<Person> receivers = new HashSet<>();
for (int i = 0; i < 3; i++) {
receivers.add(personRepository.save(
Person.builder().firstName("R" + i).lastName("FbReceiver").build()));
}
Set<Tag> tags = new HashSet<>();
for (int i = 0; i < 5; i++) {
tags.add(tagRepository.save(Tag.builder().name("FbTag" + i).build()));
}
Document doc = documentRepository.save(Document.builder()
.title("FindByIdQc").originalFilename("findbyid_qc.pdf")
.status(DocumentStatus.UPLOADED)
.sender(sender).receivers(receivers).tags(tags)
.build());
entityManager.flush();
entityManager.clear();
Statistics stats = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
stats.clear();
documentRepository.findById(doc.getId());
assertThat(stats.getPrepareStatementCount())
.as("@EntityGraph(Document.full) must load sender+receivers+tags in ≤2 statements, not 4")
.isLessThanOrEqualTo(2);
}
// ─── seeding helpers ─────────────────────────────────────────────────────
private Document uploaded(String title) {

View File

@@ -13,6 +13,10 @@ spring:
password: test
mail:
host: localhost
jpa:
properties:
hibernate:
generate_statistics: true
# Disable OTel SDK entirely in tests — prevents auto-configuration from loading resource providers
# (e.g. AzureAppServiceResourceProvider) that fail against the semconv version used here.