From 2506523f3bf176fa88291b20c088288599226692 Mon Sep 17 00:00:00 2001 From: Marcel Date: Tue, 5 May 2026 07:48:26 +0200 Subject: [PATCH] refactor(transcription/annotation): break mutual repo dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TranscriptionService injected AnnotationRepository; AnnotationService injected TranscriptionBlockRepository. Each side now talks through the other domain's service: - TranscriptionService.deleteByAnnotationId — new write delegation; called from AnnotationService.deleteAnnotation in place of the foreign repo. - AnnotationService.deleteById / deleteAllById — new write delegations; called from TranscriptionService for cascading annotation cleanup. - AnnotationService.findById (added in #417 commit 6) replaces the read. - @Lazy on AnnotationService's TranscriptionService field breaks the resulting two-bean cycle at construction time, mirroring the existing @Lazy self-reference pattern in SenderModelService. Refs #417 (C6.2 violations #10 and #11). Co-Authored-By: Claude Opus 4.7 --- .../service/AnnotationService.java | 20 ++++++++++++++++--- .../service/TranscriptionService.java | 13 +++++++----- .../service/AnnotationServiceTest.java | 9 ++++----- .../TranscriptionServiceGuidedTest.java | 5 +---- .../service/TranscriptionServiceTest.java | 8 +++----- 5 files changed, 33 insertions(+), 22 deletions(-) diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java index 1484c796..2250e8fc 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/AnnotationService.java @@ -10,7 +10,7 @@ import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.exception.ErrorCode; import org.raddatz.familienarchiv.model.DocumentAnnotation; import org.raddatz.familienarchiv.repository.AnnotationRepository; -import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; +import org.springframework.context.annotation.Lazy; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,7 +26,11 @@ import java.util.UUID; public class AnnotationService { private final AnnotationRepository annotationRepository; - private final TranscriptionBlockRepository blockRepository; + // @Lazy: AnnotationService and TranscriptionService have a mutual cleanup + // dependency (deleting an annotation cascades to its blocks; deleting a block + // cascades to its annotation). Lazy resolution lets Spring construct both beans. + @Lazy + private final TranscriptionService transcriptionService; private final AuditService auditService; public List listAnnotations(UUID documentId) { @@ -37,6 +41,16 @@ public class AnnotationService { return annotationRepository.findById(id); } + @Transactional + public void deleteById(UUID annotationId) { + annotationRepository.deleteById(annotationId); + } + + @Transactional + public void deleteAllById(java.util.Collection annotationIds) { + annotationRepository.deleteAllById(annotationIds); + } + @Transactional public DocumentAnnotation createAnnotation(UUID documentId, CreateAnnotationDTO dto, UUID userId, String fileHash) { DocumentAnnotation annotation = DocumentAnnotation.builder() @@ -108,7 +122,7 @@ public class AnnotationService { throw DomainException.forbidden("Only the annotation author can delete it"); } - blockRepository.deleteByAnnotationId(annotationId); + transcriptionService.deleteByAnnotationId(annotationId); annotationRepository.delete(annotation); } diff --git a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionService.java b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionService.java index b96f1a2a..3065eefa 100644 --- a/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionService.java +++ b/backend/src/main/java/org/raddatz/familienarchiv/service/TranscriptionService.java @@ -16,7 +16,6 @@ import org.raddatz.familienarchiv.model.DocumentAnnotation; import org.raddatz.familienarchiv.model.ScriptType; import org.raddatz.familienarchiv.model.TranscriptionBlock; import org.raddatz.familienarchiv.model.TranscriptionBlockVersion; -import org.raddatz.familienarchiv.repository.AnnotationRepository; import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.raddatz.familienarchiv.repository.TranscriptionBlockVersionRepository; import org.springframework.stereotype.Service; @@ -37,7 +36,6 @@ public class TranscriptionService { private final TranscriptionBlockRepository blockRepository; private final TranscriptionBlockVersionRepository versionRepository; - private final AnnotationRepository annotationRepository; private final AnnotationService annotationService; private final DocumentService documentService; private final SenderModelService senderModelService; @@ -47,6 +45,11 @@ public class TranscriptionService { return blockRepository.findByDocumentIdOrderBySortOrderAsc(documentId); } + @Transactional + public void deleteByAnnotationId(UUID annotationId) { + blockRepository.deleteByAnnotationId(annotationId); + } + public TranscriptionBlock getBlock(UUID documentId, UUID blockId) { return blockRepository.findByIdAndDocumentId(blockId, documentId) .orElseThrow(() -> DomainException.notFound( @@ -142,7 +145,7 @@ public class TranscriptionService { saveVersion(saved, userId); if (!text.equals(previousText)) { - Optional annotation = annotationRepository.findById(block.getAnnotationId()); + Optional annotation = annotationService.findById(block.getAnnotationId()); int pageNumber = annotation.map(DocumentAnnotation::getPageNumber).orElse(0); auditService.logAfterCommit(AuditKind.TEXT_SAVED, userId, documentId, Map.of("pageNumber", pageNumber, "blockId", saved.getId().toString())); @@ -165,7 +168,7 @@ public class TranscriptionService { // then delete the dependent annotation directly (no ownership check needed) blockRepository.delete(block); blockRepository.flush(); - annotationRepository.deleteById(annotationId); + annotationService.deleteById(annotationId); log.info("Deleted transcription block {} and annotation {} for document {}", blockId, annotationId, documentId); } @@ -181,7 +184,7 @@ public class TranscriptionService { blockRepository.deleteAll(blocks); blockRepository.flush(); - annotationRepository.deleteAllById(annotationIds); + annotationService.deleteAllById(annotationIds); log.info("Bulk-deleted {} transcription blocks for document {}", blocks.size(), documentId); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java index 19277bec..02bc6f9b 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/AnnotationServiceTest.java @@ -13,7 +13,6 @@ import org.raddatz.familienarchiv.dto.UpdateAnnotationDTO; import org.raddatz.familienarchiv.exception.DomainException; import org.raddatz.familienarchiv.model.DocumentAnnotation; import org.raddatz.familienarchiv.repository.AnnotationRepository; -import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.springframework.dao.DataIntegrityViolationException; import java.util.Map; @@ -36,7 +35,7 @@ import static org.springframework.http.HttpStatus.NOT_FOUND; class AnnotationServiceTest { @Mock AnnotationRepository annotationRepository; - @Mock TranscriptionBlockRepository blockRepository; + @Mock TranscriptionService transcriptionService; @Mock AuditService auditService; @InjectMocks AnnotationService annotationService; @@ -208,7 +207,7 @@ class AnnotationServiceTest { annotationService.deleteAnnotation(docId, annotId, ownerId); - verify(blockRepository).deleteByAnnotationId(annotId); + verify(transcriptionService).deleteByAnnotationId(annotId); verify(annotationRepository).delete(annotation); } @@ -225,8 +224,8 @@ class AnnotationServiceTest { annotationService.deleteAnnotation(docId, annotId, ownerId); - var inOrder = org.mockito.Mockito.inOrder(blockRepository, annotationRepository); - inOrder.verify(blockRepository).deleteByAnnotationId(annotId); + var inOrder = org.mockito.Mockito.inOrder(transcriptionService, annotationRepository); + inOrder.verify(transcriptionService).deleteByAnnotationId(annotId); inOrder.verify(annotationRepository).delete(annotation); } diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceGuidedTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceGuidedTest.java index 0e786d83..dc423326 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceGuidedTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceGuidedTest.java @@ -5,7 +5,6 @@ import org.junit.jupiter.api.Test; import org.raddatz.familienarchiv.audit.AuditService; import org.raddatz.familienarchiv.model.BlockSource; import org.raddatz.familienarchiv.model.TranscriptionBlock; -import org.raddatz.familienarchiv.repository.AnnotationRepository; import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.raddatz.familienarchiv.repository.TranscriptionBlockVersionRepository; @@ -20,7 +19,6 @@ class TranscriptionServiceGuidedTest { TranscriptionBlockRepository blockRepository; TranscriptionBlockVersionRepository versionRepository; - AnnotationRepository annotationRepository; AnnotationService annotationService; DocumentService documentService; SenderModelService senderModelService; @@ -35,14 +33,13 @@ class TranscriptionServiceGuidedTest { void setUp() { blockRepository = mock(TranscriptionBlockRepository.class); versionRepository = mock(TranscriptionBlockVersionRepository.class); - annotationRepository = mock(AnnotationRepository.class); annotationService = mock(AnnotationService.class); documentService = mock(DocumentService.class); senderModelService = mock(SenderModelService.class); auditService = mock(AuditService.class); service = new TranscriptionService(blockRepository, versionRepository, - annotationRepository, annotationService, documentService, senderModelService, auditService); + annotationService, documentService, senderModelService, auditService); when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(versionRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); diff --git a/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceTest.java b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceTest.java index 15a098c0..f2b44ae4 100644 --- a/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceTest.java +++ b/backend/src/test/java/org/raddatz/familienarchiv/service/TranscriptionServiceTest.java @@ -21,7 +21,6 @@ import org.raddatz.familienarchiv.model.PersonMention; import org.raddatz.familienarchiv.model.ScriptType; import org.raddatz.familienarchiv.model.TranscriptionBlock; import org.raddatz.familienarchiv.model.TranscriptionBlockVersion; -import org.raddatz.familienarchiv.repository.AnnotationRepository; import org.raddatz.familienarchiv.repository.TranscriptionBlockRepository; import org.raddatz.familienarchiv.repository.TranscriptionBlockVersionRepository; @@ -44,7 +43,6 @@ class TranscriptionServiceTest { @Mock TranscriptionBlockRepository blockRepository; @Mock TranscriptionBlockVersionRepository versionRepository; - @Mock AnnotationRepository annotationRepository; @Mock AnnotationService annotationService; @Mock DocumentService documentService; @Mock SenderModelService senderModelService; @@ -320,7 +318,7 @@ class TranscriptionServiceTest { verify(blockRepository).delete(block); verify(blockRepository).flush(); - verify(annotationRepository).deleteById(annotId); + verify(annotationService).deleteById(annotId); } @Test @@ -354,7 +352,7 @@ class TranscriptionServiceTest { verify(blockRepository).deleteAll(List.of(block1, block2)); verify(blockRepository).flush(); - verify(annotationRepository).deleteAllById(List.of(annId1, annId2)); + verify(annotationService).deleteAllById(List.of(annId1, annId2)); } @Test @@ -532,7 +530,7 @@ class TranscriptionServiceTest { when(blockRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); when(documentService.getDocumentById(any())).thenReturn( Document.builder().scriptType(ScriptType.TYPEWRITER).build()); - when(annotationRepository.findById(annotId)).thenReturn(Optional.of(annotation)); + when(annotationService.findById(annotId)).thenReturn(Optional.of(annotation)); transcriptionService.updateBlock(docId, blockId, UpdateTranscriptionBlockDTO.builder().text("new text").build(), userId);