refactor(thumbnail): route document access through DocumentService
The Thumbnail trio (ThumbnailService, ThumbnailBackfillService, ThumbnailAsyncRunner) all injected DocumentRepository directly. They now go through three new DocumentService delegations: - findById(UUID): Optional<Document> — no-throw variant for the runner's log-and-skip behaviour on missing documents. - findForThumbnailBackfill() — wraps the existing findByFilePathIsNotNullAndThumbnailKeyIsNull query. - updateThumbnailMetadata(Document) — wraps save() for the post-thumbnail entity update. DocumentService also gains @Lazy on its existing ThumbnailAsyncRunner field to break the new DocumentService ↔ ThumbnailAsyncRunner cycle. lombok.config adds @Lazy to copyableAnnotations so the field annotation reaches the generated constructor parameter. Refs #417 (C6.2 violations #2, #3, #4). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -25,6 +25,7 @@ import org.raddatz.familienarchiv.model.TrainingLabel;
|
||||
import org.raddatz.familienarchiv.model.Person;
|
||||
import org.raddatz.familienarchiv.model.Tag;
|
||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -69,6 +70,10 @@ public class DocumentService {
|
||||
private final AuditService auditService;
|
||||
private final TranscriptionBlockQueryService transcriptionBlockQueryService;
|
||||
private final AuditLogQueryService auditLogQueryService;
|
||||
// @Lazy breaks the DocumentService ↔ ThumbnailAsyncRunner cycle: the runner
|
||||
// now reaches Document data through DocumentService (per the layering rule),
|
||||
// and Spring needs a proxy here to defer the back-edge until both beans exist.
|
||||
@Lazy
|
||||
private final ThumbnailAsyncRunner thumbnailAsyncRunner;
|
||||
|
||||
public record StoreResult(Document document, boolean isNew) {}
|
||||
@@ -77,6 +82,18 @@ public class DocumentService {
|
||||
return documentRepository.count();
|
||||
}
|
||||
|
||||
public Optional<Document> findById(UUID id) {
|
||||
return documentRepository.findById(id);
|
||||
}
|
||||
|
||||
public List<Document> findForThumbnailBackfill() {
|
||||
return documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull();
|
||||
}
|
||||
|
||||
public Document updateThumbnailMetadata(Document doc) {
|
||||
return documentRepository.save(doc);
|
||||
}
|
||||
|
||||
public Map<UUID, String> findTitlesByIds(Collection<UUID> ids) {
|
||||
if (ids.isEmpty()) return Map.of();
|
||||
Map<UUID, String> titles = new HashMap<>();
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.raddatz.familienarchiv.service;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
@@ -29,7 +28,7 @@ import java.util.concurrent.TimeoutException;
|
||||
@Slf4j
|
||||
public class ThumbnailAsyncRunner {
|
||||
|
||||
private final DocumentRepository documentRepository;
|
||||
private final DocumentService documentService;
|
||||
private final ThumbnailService thumbnailService;
|
||||
|
||||
/** Per-document timeout for the whole generate() call — defense against corrupt PDFs. */
|
||||
@@ -60,7 +59,7 @@ public class ThumbnailAsyncRunner {
|
||||
*/
|
||||
@Async("thumbnailExecutor")
|
||||
public void generateAsync(UUID documentId) {
|
||||
Optional<Document> docOpt = documentRepository.findById(documentId);
|
||||
Optional<Document> docOpt = documentService.findById(documentId);
|
||||
if (docOpt.isEmpty()) {
|
||||
log.warn("Thumbnail generation skipped: document not found id={}", documentId);
|
||||
return;
|
||||
|
||||
@@ -5,7 +5,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.raddatz.familienarchiv.exception.DomainException;
|
||||
import org.raddatz.familienarchiv.exception.ErrorCode;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -37,7 +36,7 @@ public class ThumbnailBackfillService {
|
||||
LocalDateTime startedAt
|
||||
) {}
|
||||
|
||||
private final DocumentRepository documentRepository;
|
||||
private final DocumentService documentService;
|
||||
private final ThumbnailService thumbnailService;
|
||||
|
||||
private volatile BackfillStatus currentStatus = new BackfillStatus(
|
||||
@@ -57,7 +56,7 @@ public class ThumbnailBackfillService {
|
||||
LocalDateTime startedAt = LocalDateTime.now();
|
||||
List<Document> docs;
|
||||
try {
|
||||
docs = documentRepository.findByFilePathIsNotNullAndThumbnailKeyIsNull();
|
||||
docs = documentService.findForThumbnailBackfill();
|
||||
} catch (Exception e) {
|
||||
currentStatus = new BackfillStatus(State.FAILED,
|
||||
"Backfill fehlgeschlagen: " + e.getMessage(),
|
||||
|
||||
@@ -8,7 +8,6 @@ import org.apache.pdfbox.rendering.ImageType;
|
||||
import org.apache.pdfbox.rendering.PDFRenderer;
|
||||
import org.raddatz.familienarchiv.model.Document;
|
||||
import org.raddatz.familienarchiv.model.ThumbnailAspect;
|
||||
import org.raddatz.familienarchiv.repository.DocumentRepository;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
@@ -62,16 +61,16 @@ public class ThumbnailService {
|
||||
|
||||
private final FileService fileService;
|
||||
private final S3Client s3Client;
|
||||
private final DocumentRepository documentRepository;
|
||||
private final DocumentService documentService;
|
||||
|
||||
@Value("${app.s3.bucket}")
|
||||
private String bucketName;
|
||||
|
||||
public ThumbnailService(FileService fileService, S3Client s3Client,
|
||||
DocumentRepository documentRepository) {
|
||||
DocumentService documentService) {
|
||||
this.fileService = fileService;
|
||||
this.s3Client = s3Client;
|
||||
this.documentRepository = documentRepository;
|
||||
this.documentService = documentService;
|
||||
}
|
||||
|
||||
public Outcome generate(Document doc) {
|
||||
@@ -167,7 +166,7 @@ public class ThumbnailService {
|
||||
doc.setThumbnailGeneratedAt(LocalDateTime.now());
|
||||
doc.setThumbnailAspect(result.aspect());
|
||||
doc.setPageCount(result.pageCount());
|
||||
documentRepository.save(doc);
|
||||
documentService.updateThumbnailMetadata(doc);
|
||||
return Outcome.SUCCESS;
|
||||
} catch (Exception e) {
|
||||
// Thumbnail is already in S3 but the entity update failed. Because the S3
|
||||
|
||||
Reference in New Issue
Block a user