From 522d1f3ec9f21bed01dbf72cbb38e1ac5f77ccf6 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 9 Jun 2026 22:58:13 +0200 Subject: [PATCH] fix(search): load relevance-path documents with Document.list entity graph The pure-text RELEVANCE fast path loaded documents via plain findAllById, which carries no entity graph. With Document.tags LAZY (ADR-022) and no surrounding transaction, resolveDocumentTagColors hit the dead proxy and every q-only search (document picker typeaheads) failed with 500 LazyInitializationException. Dedicated findByIdIn declares the same fetch shape as the other search loaders. Co-Authored-By: Claude Fable 5 --- .../document/DocumentRepository.java | 7 ++++++ .../document/DocumentService.java | 2 +- .../document/DocumentLazyLoadingTest.java | 22 +++++++++++++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java index 1cfe26f6..72a9fd03 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentRepository.java @@ -36,6 +36,13 @@ public interface DocumentRepository extends JpaRepository, JpaSp @EntityGraph("Document.list") Page findAll(Pageable pageable); + // Loader for the relevance fast path: list-item enrichment reads tags after the + // repository call returns, so the fetch shape must match the spec-based findAll + // overloads above. Plain findAllById carries no entity graph and must not feed + // enrichItems — see DocumentService.relevanceSortedPageFromSql. + @EntityGraph("Document.list") + List findByIdIn(Collection ids); + // Findet ein Dokument anhand des ursprünglichen Dateinamens // Wichtig für den Abgleich beim Excel-Import & Datei-Upload Optional findByOriginalFilename(String originalFilename); diff --git a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java index d4715f40..3c9814ec 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/document/DocumentService.java @@ -858,7 +858,7 @@ public class DocumentService { rankMap.put(ftsPage.hits().get(i).id(), i); pageIds.add(ftsPage.hits().get(i).id()); } - List docs = documentRepository.findAllById(pageIds).stream() + List docs = documentRepository.findByIdIn(pageIds).stream() .sorted(Comparator.comparingInt(d -> rankMap.getOrDefault(d.getId(), Integer.MAX_VALUE))) .toList(); return buildResultPaged(docs, text, pageable, ftsPage.total()); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java index 6768991f..1197577c 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/document/DocumentLazyLoadingTest.java @@ -131,6 +131,28 @@ class DocumentLazyLoadingTest { .doesNotThrowAnyException(); } + @Test + void searchDocuments_pureTextRelevance_doesNotThrowLazyInitializationException() { + // q + default sort + no other filters → the relevance fast path + // (relevanceSortedPageFromSql), which loads documents by id outside any + // transaction and must still deliver an initialized tags collection. + Person sender = savedPerson("Hans", "FtSender"); + Tag tag = savedTag("FtTag"); + savedDocument("Brief von Walter", "ft_doc.pdf", sender, Set.of(), Set.of(tag)); + + SearchFilters textOnly = new SearchFilters( + "Walter", null, null, null, null, null, null, null, null, false); + + DocumentSearchResult result = documentService.searchDocuments( + textOnly, null, "DESC", PageRequest.of(0, 10)); + + assertThat(result.totalElements()).isEqualTo(1); + assertThatCode(() -> + result.items().forEach(i -> i.tags().size())) + .doesNotThrowAnyException(); + assertThat(result.items().getFirst().tags()).extracting(Tag::getName).containsExactly("FtTag"); + } + @Test void searchDocuments_senderSort_doesNotThrowLazyInitializationException() { Person sender = savedPerson("Hans", "SsSender");