Skip to content

fix(service): add transactional support to author delete and update methods#653

Merged
zachyale merged 1 commit into
grimmory-tools:developfrom
balazs-szucs:author-delete-lazy-init
Apr 20, 2026
Merged

fix(service): add transactional support to author delete and update methods#653
zachyale merged 1 commit into
grimmory-tools:developfrom
balazs-szucs:author-delete-lazy-init

Conversation

@balazs-szucs

@balazs-szucs balazs-szucs commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Description

Linked Issue: Fixes #

Changes

this is a confirmed fix for #492

Summary by CodeRabbit

  • Refactor
    • Improved transaction management for author metadata operations to enhance system reliability during data modifications.

@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Added Spring @Transactional support to AuthorMetadataService by annotating the class with @Transactional(readOnly = true) and marking seven mutating methods with @Transactional overrides to enable write operations. Read operations inherit the class-level read-only default.

Changes

Cohort / File(s) Summary
Transactional Annotation Support
booklore-api/src/main/java/org/booklore/service/AuthorMetadataService.java
Added class-level @Transactional(readOnly = true) annotation and explicitly marked mutating methods (matchAuthor, quickMatchAuthor, unmatchAuthors, deleteAuthors, uploadAuthorPhoto, updateAuthor, uploadAuthorPhotoFromUrl) with @Transactional to override read-only default. Imported @Transactional annotation.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested labels

backend, enhancement

Poem

🐰 With transactional grace, the service now knows,
Read-only by heart, but mutations still flow,
Seven methods rise up with their @Transactional cheer,
Database safety and speed, now perfectly clear!


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description includes the required template structure with Description and Changes sections, but the Changes section lacks detail and the Linked Issue field is incomplete. Complete the 'Linked Issue' field with the actual issue number (#492) instead of leaving it empty, and provide more detailed information in the Changes section about what was modified.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title follows the conventional commit format with 'fix(service):' prefix and clearly describes the main change of adding transactional support.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified 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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@balazs-szucs balazs-szucs marked this pull request as ready for review April 18, 2026 18:12
@balazs-szucs balazs-szucs linked an issue Apr 18, 2026 that may be closed by this pull request
1 task

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 | 🟡 Minor

Rollback semantics: file deletion is not transactional.

fileService.deleteAuthorImages(authorId) is called at line 206 before authorRepository.delete(author). If the subsequent delete (or commit, e.g., FK/constraint violation from the BookMetadataEntity association 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., via TransactionSynchronizationManager.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 unmatchAuthors at 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 | 🟠 Major

Same tx-scope concern for photo upload methods.

uploadAuthorPhoto holds the write transaction across FileService.readImage, fileService.saveAuthorImages (and image.flush()), and uploadAuthorPhotoFromUrl holds it across fileService.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 the findById needs the persistence context.

Suggested narrowing: drop @Transactional here and either (a) rely on a readOnly = true lookup 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 Service bean ... 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."

🤖 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 | 🟠 Major

External HTTP and file I/O executed inside the @Transactional boundary.

Both matchAuthor and quickMatchAuthor perform 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 @Transactional method for the save, and run the HTTP fetch and thumbnail download between them, outside any persistence context.

Based on learnings: "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 ... lookup into a dedicated Service bean ... 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."

🤖 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 | 🔴 Critical

Self-invocation in autoMatchAuthors bypasses @Transactional on quickMatchAuthor, and transaction wraps file I/O and external HTTP calls.

At line 157, autoMatchAuthors calls quickMatchAuthor(authorId, "us") within a Mono.fromCallable lambda. The Spring proxy is not invoked for this self-call, so the @Transactional annotation on quickMatchAuthor (line 127) is silently bypassed. Additionally, even if the proxy were used, the execution runs on boundedElastic scheduler, which executes on a worker thread not bound to the transaction context.

More critically, quickMatchAuthor wraps multiple concerns in a single @Transactional boundary:

  • 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 @Service bean with @Transactional(readOnly = true) so file I/O and external calls run outside the persistence context. Do not use TransactionTemplate or @Lazy self-references for this.

Required: Extract a new @Service bean (e.g., AuthorEntityLoader) to handle the entity lookup and metadata apply in isolation under a read-only transaction. Call this bean from both matchAuthor and autoMatchAuthors, 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

📥 Commits

Reviewing files that changed from the base of the PR and between afe44fc and 782993d.

📒 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 @Autowired field 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

@zachyale zachyale merged commit fe057e8 into grimmory-tools:develop Apr 20, 2026
17 checks passed
dsmouse pushed a commit to dsmouse/grimmory that referenced this pull request May 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deletion of author results in unhandled exception

2 participants