From 1f3f879f9c7159fa9c04ed57b763dad3bb40e482 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 28 Apr 2026 22:14:07 +0200 Subject: [PATCH] test(transcription): JOIN FETCH query loads all block mentions for propagation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add findByPersonIdWithMentionsFetched to TranscriptionBlockRepository: subquery finds blocks referencing the renamed person, outer JOIN FETCH loads their full mentionedPersons collection. Avoids N+1 lazy selects in the propagation listener. Filtered JOIN FETCH (WHERE m.personId=:personId) was rejected — it loads only one mention entry per block, risking data loss on saveAllAndFlush. Co-Authored-By: Claude Sonnet 4.6 --- .../TranscriptionBlockRepository.java | 9 ++++++ ...nscriptionBlockMentionsRepositoryTest.java | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java b/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java index 4a226bd6..fa8ef659 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/repository/TranscriptionBlockRepository.java @@ -31,6 +31,15 @@ public interface TranscriptionBlockRepository extends JpaRepository findByMentionedPersons_PersonId(UUID personId); + @Query(""" + SELECT DISTINCT b FROM TranscriptionBlock b + JOIN FETCH b.mentionedPersons + WHERE b.id IN ( + SELECT bb.id FROM TranscriptionBlock bb JOIN bb.mentionedPersons m WHERE m.personId = :personId + ) + """) + List findByPersonIdWithMentionsFetched(@Param("personId") UUID personId); + void deleteByAnnotationId(UUID annotationId); int countByDocumentId(UUID documentId); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/repository/TranscriptionBlockMentionsRepositoryTest.java b/backend/src/test/java/org/raddatz/familienarchiv/repository/TranscriptionBlockMentionsRepositoryTest.java index f6144e4a..87af6bd5 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/repository/TranscriptionBlockMentionsRepositoryTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/repository/TranscriptionBlockMentionsRepositoryTest.java @@ -92,4 +92,36 @@ class TranscriptionBlockMentionsRepositoryTest { TranscriptionBlock reloaded = blockRepository.findById(saved.getId()).orElseThrow(); assertThat(reloaded.getMentionedPersons()).isEmpty(); } + + @Test + void findByPersonIdWithMentionsFetched_returnsOnlyBlocksReferencingPerson_withMentionsLoaded() { + UUID augusteId = UUID.randomUUID(); + UUID hermannId = UUID.randomUUID(); + + blockRepository.saveAndFlush(TranscriptionBlock.builder() + .annotationId(annotationId).documentId(documentId) + .text("Brief von @Auguste Raddatz an @Hermann Müller.") + .sortOrder(0) + .mentionedPersons(List.of( + new PersonMention(augusteId, "Auguste Raddatz"), + new PersonMention(hermannId, "Hermann Müller"))) + .build()); + blockRepository.saveAndFlush(TranscriptionBlock.builder() + .annotationId(annotationId).documentId(documentId) + .text("Unrelated block without Auguste.") + .sortOrder(1) + .mentionedPersons(List.of(new PersonMention(hermannId, "Hermann Müller"))) + .build()); + em.clear(); + + List result = + blockRepository.findByPersonIdWithMentionsFetched(augusteId); + + assertThat(result).hasSize(1); + assertThat(result.get(0).getMentionedPersons()) + .extracting(PersonMention::getPersonId, PersonMention::getDisplayName) + .containsExactlyInAnyOrder( + org.assertj.core.groups.Tuple.tuple(augusteId, "Auguste Raddatz"), + org.assertj.core.groups.Tuple.tuple(hermannId, "Hermann Müller")); + } }