fix(service): add transactional support to author delete and update methods#653
Conversation
📝 WalkthroughWalkthroughAdded Spring Changes
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Suggested labels
Poem
Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
✨ Simplify code
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java (4)
192-212:⚠️ Potential issue | 🟡 MinorRollback semantics: file deletion is not transactional.
fileService.deleteAuthorImages(authorId)is called at line 206 beforeauthorRepository.delete(author). If the subsequentdelete(or commit, e.g., FK/constraint violation from theBookMetadataEntityassociation cleanup at 200–204) fails, the images are already gone on disk but the row remains — permanent data inconsistency. Consider deleting files only after the transaction commits (e.g., viaTransactionSynchronizationManager.registerSynchronization/@TransactionalEventListener(phase = AFTER_COMMIT)), or reorder so the DB delete is flushed first and files are removed post-commit.The same observation applies to
unmatchAuthorsat line 185.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java` around lines 192 - 212, The deleteAuthors method currently calls fileService.deleteAuthorImages(authorId) before the DB delete, which can leave files removed if the transaction later rolls back; move file deletion to after successful commit by deferring deletion (either register a post-commit callback with TransactionSynchronizationManager.registerSynchronization or publish an event handled by a `@TransactionalEventListener`(phase = AFTER_COMMIT)) so that fileService.deleteAuthorImages runs only after authorRepository.delete(...) has committed; apply the same deferral pattern to unmatchAuthors where fileService.deleteAuthorImages is invoked.
214-278:⚠️ Potential issue | 🟠 MajorSame tx-scope concern for photo upload methods.
uploadAuthorPhotoholds the write transaction acrossFileService.readImage,fileService.saveAuthorImages(andimage.flush()), anduploadAuthorPhotoFromUrlholds it acrossfileService.createAuthorThumbnailFromUrl(which typically downloads from a remote URL and writes to disk). No DB mutation actually happens after the initial lookup in either method — only thefindByIdneeds the persistence context.Suggested narrowing: drop
@Transactionalhere and either (a) rely on areadOnly = truelookup via an extracted loader bean, or (b) move the lookup into a short@Transactional(readOnly = true)method and do all I/O afterward. This also avoids holding a DB connection for the duration of potentially large image decodes and network fetches.Based on learnings: "Option B: extract the ... lookup into a dedicated
Servicebean ... annotated withTransactional(readOnly = true), so that the proxy always narrows the transaction to the DB access only and all filesystem I/O runs outside the persistence context."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java` around lines 214 - 278, Both uploadAuthorPhoto and uploadAuthorPhotoFromUrl keep a write transaction open for long-running I/O (FileService.readImage, fileService.saveAuthorImages, image.flush, fileService.createAuthorThumbnailFromUrl) even though only the initial authorRepository.findById call needs DB access; extract the DB lookup into a small transactional(readOnly = true) helper (e.g., loadAuthorOrThrow(Long) annotated with `@Transactional`(readOnly = true)) or move the findById call into a dedicated loader bean, then remove `@Transactional` from uploadAuthorPhoto and uploadAuthorPhotoFromUrl so all image decoding/networking (FileService.readImage, fileService.saveAuthorImages, fileService.createAuthorThumbnailFromUrl) runs outside the persistence context and the connection is not held during I/O.
98-149:⚠️ Potential issue | 🟠 MajorExternal HTTP and file I/O executed inside the
@Transactionalboundary.Both
matchAuthorandquickMatchAuthorperform remote provider calls (provider.getAuthorByAsin/provider.quickSearch) and filesystem/URL I/O (fileService.createAuthorThumbnailFromUrl) while a write transaction — and therefore a JDBC connection — is held open. Slow/hung upstreams will pin connections from the pool and can cascade into outages; this is exactly the anti-pattern called out for this codebase.Preferred refactor (Option B from prior learnings): keep the transactional boundary narrow to DB access only. Extract an
AuthorEntityLoader/writer bean with@Transactional(readOnly = true)for the lookup and a separate@Transactionalmethod for the save, and run the HTTP fetch and thumbnail download between them, outside any persistence context.Based on learnings: "for the
AudiobookReaderServicepattern of wrapping a DB lookup + file I/O under a singleTransactionalboundary, the preferred refactoring approach is Option B: extract the ... lookup into a dedicatedServicebean ... annotated withTransactional(readOnly = true), so that the proxy always narrows the transaction to the DB access only and all filesystem I/O runs outside the persistence context."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java` around lines 98 - 149, Both matchAuthor and quickMatchAuthor hold a DB transaction while calling external HTTP (provider.getAuthorByAsin / provider.quickSearch) and doing file I/O (fileService.createAuthorThumbnailFromUrl); refactor to narrow transactions by extracting a new AuthorEntityLoader bean with a `@Transactional`(readOnly = true) method (e.g., loadAuthorForMatch(Long authorId) that returns AuthorEntity or throws ApiError.AUTHOR_NOT_FOUND) and a separate `@Transactional` writer method (e.g., saveAuthorMetadata(AuthorEntity) or in AuthorMetadataService.saveAuthor). Change matchAuthor and quickMatchAuthor to: call the loader to fetch the AuthorEntity (within readOnly tx), then perform provider calls and thumbnail download outside any transaction, then call the transactional save method to persist changes (use applyMetadataResult before save); keep auditService.log after successful save. Ensure you reference and update usages of provider.getAuthorByAsin, provider.quickSearch, fileService.createAuthorThumbnailFromUrl, applyMetadataResult, authorRepository.save and auditService.log accordingly.
151-174:⚠️ Potential issue | 🔴 CriticalSelf-invocation in
autoMatchAuthorsbypasses@TransactionalonquickMatchAuthor, and transaction wraps file I/O and external HTTP calls.At line 157,
autoMatchAuthorscallsquickMatchAuthor(authorId, "us")within aMono.fromCallablelambda. The Spring proxy is not invoked for this self-call, so the@Transactionalannotation onquickMatchAuthor(line 127) is silently bypassed. Additionally, even if the proxy were used, the execution runs onboundedElasticscheduler, which executes on a worker thread not bound to the transaction context.More critically,
quickMatchAuthorwraps multiple concerns in a single@Transactionalboundary:
- External HTTP calls via
provider.quickSearch()(line 132)- File I/O via
fileService.createAuthorThumbnailFromUrl()(line 137)- DB writes via
authorRepository.save()(line 135)This pattern holds a DB connection throughout external I/O and network operations, which is the anti-pattern documented in the learning (PR 513): extract the DB lookup into a separate
@Servicebean with@Transactional(readOnly = true)so file I/O and external calls run outside the persistence context. Do not useTransactionTemplateor@Lazyself-references for this.Required: Extract a new
@Servicebean (e.g.,AuthorEntityLoader) to handle the entity lookup and metadata apply in isolation under a read-only transaction. Call this bean from bothmatchAuthorandautoMatchAuthors, and move all file I/O and external provider calls outside the transaction.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java` around lines 151 - 174, autoMatchAuthors is self-calling quickMatchAuthor and running it on boundedElastic which bypasses the Spring transactional proxy and holds a transaction across I/O and HTTP calls; fix by extracting the DB lookup into a new `@Service` bean (e.g., AuthorEntityLoader) with a method like loadAuthorEntityForMatch(Long authorId) annotated `@Transactional`(readOnly = true) that fetches and returns the AuthorEntity (or a detached DTO) only; update quickMatchAuthor and autoMatchAuthors to call AuthorEntityLoader.loadAuthorEntityForMatch(authorId) first, then perform provider.quickSearch(...), fileService.createAuthorThumbnailFromUrl(...) and Files.exists(...) outside that transaction on the boundedElastic scheduler, and only persist changes with a separate transactional save call (or by moving repository.save into a brief `@Transactional` method) after external I/O completes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java`:
- Around line 192-212: The deleteAuthors method currently calls
fileService.deleteAuthorImages(authorId) before the DB delete, which can leave
files removed if the transaction later rolls back; move file deletion to after
successful commit by deferring deletion (either register a post-commit callback
with TransactionSynchronizationManager.registerSynchronization or publish an
event handled by a `@TransactionalEventListener`(phase = AFTER_COMMIT)) so that
fileService.deleteAuthorImages runs only after authorRepository.delete(...) has
committed; apply the same deferral pattern to unmatchAuthors where
fileService.deleteAuthorImages is invoked.
- Around line 214-278: Both uploadAuthorPhoto and uploadAuthorPhotoFromUrl keep
a write transaction open for long-running I/O (FileService.readImage,
fileService.saveAuthorImages, image.flush,
fileService.createAuthorThumbnailFromUrl) even though only the initial
authorRepository.findById call needs DB access; extract the DB lookup into a
small transactional(readOnly = true) helper (e.g., loadAuthorOrThrow(Long)
annotated with `@Transactional`(readOnly = true)) or move the findById call into a
dedicated loader bean, then remove `@Transactional` from uploadAuthorPhoto and
uploadAuthorPhotoFromUrl so all image decoding/networking
(FileService.readImage, fileService.saveAuthorImages,
fileService.createAuthorThumbnailFromUrl) runs outside the persistence context
and the connection is not held during I/O.
- Around line 98-149: Both matchAuthor and quickMatchAuthor hold a DB
transaction while calling external HTTP (provider.getAuthorByAsin /
provider.quickSearch) and doing file I/O
(fileService.createAuthorThumbnailFromUrl); refactor to narrow transactions by
extracting a new AuthorEntityLoader bean with a `@Transactional`(readOnly = true)
method (e.g., loadAuthorForMatch(Long authorId) that returns AuthorEntity or
throws ApiError.AUTHOR_NOT_FOUND) and a separate `@Transactional` writer method
(e.g., saveAuthorMetadata(AuthorEntity) or in AuthorMetadataService.saveAuthor).
Change matchAuthor and quickMatchAuthor to: call the loader to fetch the
AuthorEntity (within readOnly tx), then perform provider calls and thumbnail
download outside any transaction, then call the transactional save method to
persist changes (use applyMetadataResult before save); keep auditService.log
after successful save. Ensure you reference and update usages of
provider.getAuthorByAsin, provider.quickSearch,
fileService.createAuthorThumbnailFromUrl, applyMetadataResult,
authorRepository.save and auditService.log accordingly.
- Around line 151-174: autoMatchAuthors is self-calling quickMatchAuthor and
running it on boundedElastic which bypasses the Spring transactional proxy and
holds a transaction across I/O and HTTP calls; fix by extracting the DB lookup
into a new `@Service` bean (e.g., AuthorEntityLoader) with a method like
loadAuthorEntityForMatch(Long authorId) annotated `@Transactional`(readOnly =
true) that fetches and returns the AuthorEntity (or a detached DTO) only; update
quickMatchAuthor and autoMatchAuthors to call
AuthorEntityLoader.loadAuthorEntityForMatch(authorId) first, then perform
provider.quickSearch(...), fileService.createAuthorThumbnailFromUrl(...) and
Files.exists(...) outside that transaction on the boundedElastic scheduler, and
only persist changes with a separate transactional save call (or by moving
repository.save into a brief `@Transactional` method) after external I/O
completes.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 6ad7dcd0-482d-4091-bdd9-db3041d3236e
📒 Files selected for processing (1)
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (1)
booklore-api/src/**/*.java
📄 CodeRabbit inference engine (AGENTS.md)
booklore-api/src/**/*.java: Use 4-space indentation in Java files and match surrounding Java style
Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce@Autowiredfield injection
Use MapStruct for entity/DTO mapping
Files:
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
🧠 Learnings (7)
📓 Common learnings
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 513
File: booklore-api/src/main/java/org/booklore/service/reader/AudiobookReaderService.java:38-39
Timestamp: 2026-04-14T22:11:02.436Z
Learning: In `grimmory-tools/grimmory`, for the `AudiobookReaderService` pattern of wrapping a DB lookup + file I/O under a single `Transactional` boundary, the preferred refactoring approach is **Option B**: extract the `BookFileEntity` lookup into a dedicated `Service` bean (e.g., `AudiobookEntityLoader`) annotated with `Transactional(readOnly = true)`, so that the proxy always narrows the transaction to the DB access only and all filesystem I/O runs outside the persistence context. Do NOT use `TransactionTemplate` (Option A) for this pattern in this codebase.
📚 Learning: 2026-04-14T22:11:02.436Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 513
File: booklore-api/src/main/java/org/booklore/service/reader/AudiobookReaderService.java:38-39
Timestamp: 2026-04-14T22:11:02.436Z
Learning: In `grimmory-tools/grimmory`, for the `AudiobookReaderService` pattern of wrapping a DB lookup + file I/O under a single `Transactional` boundary, the preferred refactoring approach is **Option B**: extract the `BookFileEntity` lookup into a dedicated `Service` bean (e.g., `AudiobookEntityLoader`) annotated with `Transactional(readOnly = true)`, so that the proxy always narrows the transaction to the DB access only and all filesystem I/O runs outside the persistence context. Do NOT use `TransactionTemplate` (Option A) for this pattern in this codebase.
Applied to files:
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
📚 Learning: 2026-03-26T01:46:48.863Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-26T01:46:48.863Z
Learning: Applies to booklore-api/src/**/*.java : Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce `Autowired` field injection
Applied to files:
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
📚 Learning: 2026-04-10T08:15:37.436Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 449
File: booklore-api/src/main/java/org/booklore/service/book/BookDownloadService.java:139-145
Timestamp: 2026-04-10T08:15:37.436Z
Learning: When using Spring `ContentDisposition.builder(...).filename(name, StandardCharsets.UTF_8).build()` (i.e., explicitly providing UTF-8), the resulting header value should include both the quoted `filename="=?UTF-8?..."` and the RFC 5987 `filename*=` parameters. In this case, any extra ASCII fallback computation (e.g., deriving an ASCII `fallbackFilename` via `NON_ASCII_PATTERN` and calling `.filename(fallbackFilename)`) is likely redundant—prefer calling only `.filename(fallbackName?, StandardCharsets.UTF_8)` as appropriate and let Spring handle the UTF-8 header parameters. Verify by comparing the emitted header for `filename` and `filename*` before deciding to keep an ASCII fallback.
Applied to files:
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
📚 Learning: 2026-04-14T12:43:08.698Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 502
File: booklore-api/src/main/java/org/booklore/service/reader/ChapterCacheService.java:0-0
Timestamp: 2026-04-14T12:43:08.698Z
Learning: For this codebase (booklore-api), target Java 25 with `--enable-preview`, so `_` is intentionally used as an unnamed/ignored variable (e.g., lambda parameter or pattern variable) per Java’s preview feature JEP 456. Do not flag `_` in those contexts as an invalid/reserved identifier; only flag it if it’s used in a non-supported position (e.g., where an unnamed variable is not applicable for the Java preview rules).
Applied to files:
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
📚 Learning: 2026-04-13T05:02:01.463Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 486
File: booklore-api/src/main/java/org/booklore/service/oidc/OidcGroupMappingService.java:130-159
Timestamp: 2026-04-13T05:02:01.463Z
Learning: In grimmory-tools/grimmory, treat `permissionDemoUser`/`permission_demo_user` as a restricting “limited demo mode” flag: it must never be set to `true` as part of any “grant all permissions” or admin-grant flow. Specifically, in code paths that grant admin permissions (for example the OIDC `isAdmin` branch in `OidcGroupMappingService.applyPermissions`), ensure `permissionDemoUser` is intentionally excluded from the admin-permission block and cannot be turned on; enabling it for admin users is counterproductive.
Applied to files:
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
📚 Learning: 2026-04-14T12:46:56.673Z
Learnt from: balazs-szucs
Repo: grimmory-tools/grimmory PR: 502
File: booklore-api/src/main/java/org/booklore/service/reader/ChapterCacheService.java:0-0
Timestamp: 2026-04-14T12:46:56.673Z
Learning: For this project (grimmory-tools/grimmory / booklore-api), prefer modern Java 25 idioms and language/library features when reviewing or suggesting changes. Favor current constructs such as unnamed variables (e.g., `_`) where applicable, records, pattern matching for `switch`/`instanceof`, sealed classes, sequenced collections, `Stream.toList()`, and text blocks. Avoid proposing regressions to older/legacy equivalents unless there’s a concrete compatibility constraint (e.g., a required older runtime/library behavior).
Applied to files:
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
Description
Linked Issue: Fixes #
Changes
this is a confirmed fix for #492
Summary by CodeRabbit