From ff792e6625384c80669b6286824581acdd3ba31c Mon Sep 17 00:00:00 2001 From: Marcel Date: Mon, 18 May 2026 16:43:36 +0200 Subject: [PATCH] test(document): add query-count assertions for findAll + findById entity graphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../document/DocumentRepositoryTest.java | 70 +++++++++++++++++++ .../src/test/resources/application-test.yaml | 4 ++ 2 files changed, 74 insertions(+) diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java index dabf6dae..abaa9a5b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentRepositoryTest.java @@ -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 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 receivers = new HashSet<>(); + for (int i = 0; i < 3; i++) { + receivers.add(personRepository.save( + Person.builder().firstName("R" + i).lastName("FbReceiver").build())); + } + Set 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) { diff --git a/backend/src/test/resources/application-test.yaml b/backend/src/test/resources/application-test.yaml index e1a2f913..d5644a9d 100644 --- a/backend/src/test/resources/application-test.yaml +++ b/backend/src/test/resources/application-test.yaml @@ -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.