Skip to content

fix(hibernate): improve transaction management and optimize entity fetching#176

Merged
balazs-szucs merged 21 commits into
grimmory-tools:developfrom
balazs-szucs:hibernate-lobs
Apr 3, 2026
Merged

fix(hibernate): improve transaction management and optimize entity fetching#176
balazs-szucs merged 21 commits into
grimmory-tools:developfrom
balazs-szucs:hibernate-lobs

Conversation

@balazs-szucs

@balazs-szucs balazs-szucs commented Mar 24, 2026

Copy link
Copy Markdown
Contributor

Description

Linked Issue: Fixes #

Changes

This pull request introduces several improvements and optimizations across the codebase, focusing on entity mapping efficiency, transactional consistency, and controller enhancements. The main changes include improved lazy loading and batch fetching for JPA entities, transactional annotation adjustments for service classes, a new paginated endpoint in the book controller, and some code cleanups for better performance and maintainability.

Entity mapping and performance improvements:

  • Switched several entity collection fields (e.g., bookMetadataEntityList in AuthorEntity, shelves and userBookProgress in BookEntity) from lists to sets and initialized them with default empty collections. Added @BatchSize and @LazyGroup annotations to optimize lazy loading and batch fetching, and made certain large fields (like embeddingVector and searchText in BookMetadataEntity) lazy-loaded for performance.

  • Added or improved equals and hashCode implementations for BookLoreUserEntity to ensure correct entity comparison, using Hibernate's class-based equality.

Service and transactional consistency:

  • Moved the @Transactional(readOnly = true) annotation from individual methods to the class level in AppBookService to ensure consistent transactional behavior for all service methods.

  • Added @Transactional to the logout method in LogoutService and optimized token revocation by collecting tokens and saving them in bulk with saveAll instead of saving each individually.

Controller and API enhancements:

  • Added a new paginated endpoint /page in BookController to support retrieving books with pagination and sorting using Spring Data's Pageable.

Build and configuration improvements:

  • Updated Hibernate enhancement settings in build.gradle.kts to enable lazy initialization and increased the test task's heap size for better test performance.

Summary by CodeRabbit

  • New Features

    • Paginated book listing for easier browsing.
    • Embeddings-based recommendations for more relevant suggestions.
    • Faster, more accurate “Continue reading / Continue listening” lists using pre-ranked results.
  • Performance Improvements

    • Reduced eager loading; more batched and deferred data loading.
    • Batch processing for embedding/recommendation updates and smaller in-memory caches.
  • Bug Fixes

    • More reliable logout and refresh-token revocation.
    • Improved library rescan, upload/post-import handling, and file-sync accuracy.

@coderabbitai

coderabbitai Bot commented Mar 24, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Added class-level read-only transactions to many services, introduced paginated/batched repository APIs and an embedding projection, switched continue-reading/listening to pre-ranked ID queries, replaced SUBSELECT with batch loading and deferred LOBs, added embeddings-based recommendations and batched persistence, moved some post-import side effects into REQUIRES_NEW.

Changes

Cohort / File(s) Summary
Service transactional consolidation
booklore-api/src/main/java/org/booklore/app/service/AppBookService.java, booklore-api/src/main/java/org/booklore/service/MagicShelfService.java, booklore-api/src/main/java/org/booklore/service/ReadingSessionService.java, booklore-api/src/main/java/org/booklore/service/book/NotebookService.java, booklore-api/src/main/java/org/booklore/service/book/BookService.java, booklore-api/src/main/java/org/booklore/service/library/LibraryService.java, booklore-api/src/main/java/org/booklore/service/progress/ReadingProgressService.java, booklore-api/src/main/java/org/booklore/service/watcher/BookFilePersistenceService.java, booklore-api/src/main/java/org/booklore/service/restriction/ContentRestrictionService.java
Added class-level @Transactional(readOnly = true) and removed duplicate method-level read-only annotations; write methods keep explicit transactional annotations.
Transactional additions & boundary changes
booklore-api/src/main/java/org/booklore/config/security/service/LogoutService.java, booklore-api/src/main/java/org/booklore/service/upload/FileUploadService.java, booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java, booklore-api/src/main/java/org/booklore/service/metadata/MetadataTaskService.java, booklore-api/src/main/java/org/booklore/controller/MetadataController.java, booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java
Added/adjusted @Transactional on classes/methods; changed refresh-token revocation to collect-and-saveAll; removed explicit save after metadata updates; moved post-import side effects into a REQUIRES_NEW TransactionTemplate.
Repository refactor & pagination
booklore-api/src/main/java/org/booklore/repository/BookRepository.java, booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java, booklore-api/src/main/java/org/booklore/repository/projection/BookEmbeddingProjection.java
Narrowed entity graphs to To-One paths, removed heavy fetch variants, added paginated query variants, counts, rescan/deleted queries, embedding projection and embedding queries, and pageable continue-reading/listening ID queries.
Service paging, batch APIs & controller
booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java, booklore-api/src/main/java/org/booklore/service/book/BookService.java, booklore-api/src/main/java/org/booklore/controller/BookController.java
Added paginated endpoints/methods and repo-backed page flows, batch entity loaders, counting APIs, batch persistence helpers for embeddings/recommendations, and user progress enrichment for paged results.
Recommendation embeddings
booklore-api/src/main/java/org/booklore/service/recommender/BookRecommendationService.java, booklore-api/src/main/java/org/booklore/repository/projection/BookEmbeddingProjection.java, booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
New embeddings-based recommendation path using BookVectorService and embedding projections; computes cosine similarities, filters/sorts candidates, loads top entities and applies author caps; falls back to entity-based similarity when embeddings absent.
Batch task & tests
booklore-api/src/main/java/org/booklore/task/tasks/BookRecommendationUpdaterTask.java, .../BookRecommendationUpdaterTaskTest.java
Refactored recommendation updater to paginated/batched processing; compute and persist embeddings per-batch and persist recommendations via batched helper; tests updated for batched behavior.
Entity fetch/LOB/batching changes
booklore-api/src/main/java/org/booklore/model/entity/BookMetadataEntity.java, .../AuthorEntity.java, .../CategoryEntity.java, .../ComicMetadataEntity.java, .../MoodEntity.java, .../TagEntity.java, .../TaskHistoryEntity.java, .../BookdropFileEntity.java, .../PdfAnnotationEntity.java, .../MetadataFetchProposalEntity.java
Replaced @Fetch(FetchMode.SUBSELECT) with @BatchSize(20) for collections, added @Basic(fetch = LAZY) + @LazyGroup(...) for large LOBs, and introduced @Builder.Default initializers for collections.
Entity defaults, identity & collections
booklore-api/src/main/java/org/booklore/model/entity/BookEntity.java, .../LibraryEntity.java, .../AuthorEntity.java, .../BookLoreUserEntity.java, .../KoboLibrarySnapshotEntity.java, .../AppSettingEntity.java, .../MetadataFetchProposalEntity.java
Initialized collection fields, converted some List→Set, replaced Lombok @Data with @Getter/@Setter in some entities, and added/adjusted equals/hashCode to use identity-based semantics.
Library processing & rescan changes
booklore-api/src/main/java/org/booklore/service/library/LibraryProcessingService.java, .../LibraryService.java, .../BookRestorationService.java, .../LibraryFileEventProcessor.java
Switched rescan/new-book detection to repository-driven queries (findAllByLibraryIdForRescan), changed library fetches to findByIdWithPaths, updated helper signatures to accept List<BookEntity> + libraryId, and adapted restoration logic to use repo results.
Komga & series flows
booklore-api/src/main/java/org/booklore/service/komga/KomgaService.java, .../KomgaServiceTest.java
Replaced in-memory pagination/filtering with repository-backed pagination and series-targeted queries; series resolution uses distinct series-name queries and targeted repository loads.
Watcher / file processing
booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java, .../LibraryFileEventProcessor.java, .../service/watcher/BookFilePersistenceService.java
Moved post-import side effects into a REQUIRES_NEW TransactionTemplate; library event processing now loads paths (findByIdWithPaths); watcher persistence service set to class-level read-only default; tests updated to mock PlatformTransactionManager.
Configuration & build
booklore-api/src/main/resources/application.yaml, src/test/resources/application-test.yml, build.gradle.kts, Dockerfile, dev.docker-compose.yml
Set spring.jpa.open-in-view=false, adjusted Hikari/autocommit and Hibernate tuning (plan cache, parameter metadata size, batch sizes, fail-on-pagination-over-collection-fetch), reduced Tomcat threads, changed JVM options in Docker/dev compose, and adjusted Hibernate enhancement flags and test properties.
Tests & fixtures
many src/test/java/... (notably HibernateRegressionTest, BookOpdsRepositoryDataJpaTest, BookCreatorServiceTest, BookFileAttachmentServiceTest, BookDropServiceTest, LibraryProcessingServiceTest, KomgaServiceTest, etc.)
Added comprehensive HibernateRegressionTest; updated tests to expect initialized collections, populate bidirectional associations in helpers, adopt repository batch methods, mock PlatformTransactionManager and Flyway, and adjust stubbing/verifications for batched persistence and repo method changes.
Frontend cache tweak
frontend/src/app/features/book/service/book.service.ts
Set query staleTime to 30_000ms for the books list query options.
Small cache size adjustments
booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java, .../OidcStateService.java, .../AuthRateLimitService.java, .../LoginRateLimitService.java, .../IconService.java, .../EpubCfiService.java
Reduced various in-memory cache maximumSizes/expiry values (JTIs, OIDC state, rate-limit caches, SVG cache, epub document cache).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Rec as BookRecommendationService
    participant Repo as BookRepository
    participant Vec as BookVectorService
    participant Qry as BookQueryService

    Note over Client,Rec: Request similar books
    Client->>Rec: findSimilarBooks(bookId, limit)
    Rec->>Repo: findAllEmbeddingsForRecommendation(bookId)
    Repo-->>Rec: List<BookEmbeddingProjection>
    alt target has embedding
        Rec->>Vec: deserialize target embedding
        Vec-->>Rec: target vector
        Rec->>Vec: compute cosine similarities for candidates
        Vec-->>Rec: similarity scores
        Rec->>Repo: findAllById(selectedIds)
        Repo-->>Rec: List<BookEntity>
        Rec->>Qry: apply author caps & map to BookRecommendation
        Qry-->>Rec: List<BookRecommendation>
        Rec-->>Client: recommendations
    else fallback
        Rec->>Repo: find candidate entities (entity-based)
        Repo-->>Rec: candidate entities
        Rec->>Qry: load full entities & build recommendations
        Qry-->>Rec: List<BookRecommendation>
        Rec-->>Client: recommendations
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • zachyale
  • imajes

Poem

🐰
I nibble through queries, light and spry,
Batching hops where lazy LOBs lie,
Transactions snug in a neat new row,
Embeddings hum and recommendations grow,
Hop! — paged results bloom, then off I go.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.47% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title follows the conventional commit format with 'fix' type and a descriptive scope/subject about Hibernate transaction management and entity fetching optimization.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering entity mapping improvements, transactional consistency, controller enhancements, and build configuration updates with clear examples.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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.

This comment was marked as resolved.

coderabbitai[bot]

This comment was marked as resolved.

@imajes imajes marked this pull request as ready for review March 26, 2026 01:17

@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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
booklore-api/src/main/java/org/booklore/service/upload/FileUploadService.java (1)

63-95: ⚠️ Potential issue | 🟠 Major

Remove @Transactional from uploadFile to avoid wrapping filesystem mutations in a DB transaction.

Line 63 wraps transferTo(...), file move, and metadata extraction in a Spring transaction boundary. If a runtime exception occurs after moveFileToFinalLocation(...) completes or during transaction commit, the database transaction rolls back while the file remains in its final location, causing DB/filesystem divergence.

The audit log does not require the parent transaction—AuditService.log() uses auditLogRepository.save(), which creates its own implicit transaction and silently handles failures via try-catch.

Recommended approach
-    `@Transactional`
     public void uploadFile(MultipartFile file, long libraryId, long pathId) {
         // ... validation, file transfer, move ...
         moveFileToFinalLocation(tempPath, finalPath);
         log.info("File uploaded to final location: {}", finalPath);
-        auditService.log(AuditAction.BOOK_UPLOADED, "Library", libraryId, "Uploaded file: " + originalFileName);
+        recordUploadAudit(libraryId, originalFileName);
     }
+
+    `@Transactional`
+    protected void recordUploadAudit(long libraryId, String originalFileName) {
+        auditService.log(AuditAction.BOOK_UPLOADED, "Library", libraryId, "Uploaded file: " + originalFileName);
+    }

This moves the audit log into its own transaction, keeping filesystem mutations outside transactional boundaries.

🤖 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/upload/FileUploadService.java`
around lines 63 - 95, Remove the method-level `@Transactional` on uploadFile so
filesystem operations (file.transferTo, moveFileToFinalLocation, metadata
extraction) are not executed inside a DB transaction; keep calling
auditService.log as-is (it already handles its own persistence/transaction), and
ensure uploadFile only handles filesystem work and throws/errors as before
without a surrounding transactional boundary.
♻️ Duplicate comments (3)
booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java (1)

47-54: ⚠️ Potential issue | 🟠 Major

Pagination totals may be inaccurate after content filtering.

contentRestrictionService.applyRestrictions() can remove books from the page content, but the returned PageImpl still uses page.getTotalElements() from the original unfiltered query. This causes:

  1. Total count may include restricted books the user can't see
  2. Page may contain fewer items than pageable.getPageSize()
  3. Client may request pages beyond actual filtered content

If content restrictions are expected to be rare or minimal, this might be acceptable. Otherwise, consider applying restrictions at the query level or adjusting the total count.

🤖 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/book/BookQueryService.java`
around lines 47 - 54, getAllBooksByLibraryIdsPaged currently pages via
bookRepository.findAllWithMetadataByLibraryIdsPage then filters the page content
with contentRestrictionService.applyRestrictions, but still returns a PageImpl
with the original page.getTotalElements(), producing incorrect totals; fix by
ensuring the returned PageImpl uses a total that reflects the filtered results
(either apply restrictions at query time in
bookRepository.findAllWithMetadataByLibraryIdsPage or compute the filtered total
before returning: e.g., run the same restriction logic against the full matching
set or query for a filtered count, then build PageImpl with the filtered list
and that corrected total), and keep mapping to DTOs via mapBookToDto as before
so the content matches the adjusted total.
booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java (1)

912-923: ⚠️ Potential issue | 🟠 Major

This regression check still runs with an active session and can miss lazy-init failures.

findAllFullBooksBatch_initializesLazyCollections asserts immediately after repository fetch, so lazy relations may still resolve through the open persistence context. Detach/clear before assertions (or exercise the service boundary) to validate detached access.

Suggested fix
             List<BookEntity> batch = bookRepository.findAllFullBooksBatch(
                     PageRequest.of(0, 100));
+            entityManager.clear(); // force detached-state assertions
 
             assertThat(batch).isNotEmpty();
 
             BookEntity loaded = batch.stream()
                     .filter(b -> b.getId().equals(book.getId()))
                     .findFirst()
                     .orElseThrow();
 
             assertThat(loaded.getMetadata().getCategories()).hasSize(1);
             assertThat(loaded.getMetadata().getTitle()).isEqualTo("Batch Test Book");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java`
around lines 912 - 923, The test currently asserts on entities while the
persistence context is still open, hiding lazy-init issues; after calling
bookRepository.findAllFullBooksBatch(...) in HibernateRegressionTest (the
findAllFullBooksBatch test), clear/detach the persistence context before
asserting (e.g., call EntityManager.clear() or detach the loaded BookEntity) so
that accessing lazy collections happens on a detached entity and will surface
lazy-init failures; reference the findAllFullBooksBatch call and the BookEntity
loaded variable when applying the detach/clear.
booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java (1)

476-499: ⚠️ Potential issue | 🟠 Major

Still missing a rollback/compensation path after the import finishes.

Line 476 starts a separate post-import phase after the initial import has already completed. If anything in this block, or the commit immediately after it, fails, performFileMove() returns failure and deletes the target file, but the imported book and file records remain committed. Line 498 also publishes fileProcessResult.getBook(), so the event payload is still the pre-enrichment snapshot. Please either make import + enrichment atomic, or add compensating DB cleanup before returning failure and emit BOOK_ADD only after the post-processing transaction commits from the finalized entity/DTO.

🤖 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/bookdrop/BookDropService.java`
around lines 476 - 499, The post-import block using TransactionTemplate (newTx)
that calls metadataRefreshService.updateBookMetadata,
koboAutoShelfService.autoAddBookToKoboShelves and
notificationService.sendMessage(Topic.BOOK_ADD, fileProcessResult.getBook()) can
leave committed book/file records if it fails; either make the import +
enrichment atomic or add compensating cleanup and emit the event only after
enrichment commit. Fix by one of two approaches: (A) move the initial import and
this enrichment into a single transactional flow so the repository save and the
newTx work happen in one commit, or (B) on failure inside newTx catch/finally
perform compensating DB cleanup that deletes the created BookEntity and
associated BookFile records (using bookRepository and any file storage cleanup
used by performFileMove()), and only call notificationService.sendMessage after
successfully committing the newTx using the freshly loaded BookEntity/DTO (not
fileProcessResult.getBook()). Ensure the code references the
TransactionTemplate/newTx block, MetadataUpdateContext,
metadataRefreshService.updateBookMetadata,
koboAutoShelfService.autoAddBookToKoboShelves, and
notificationService.sendMessage so reviewers can locate and apply the change.
🧹 Nitpick comments (2)
booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java (1)

210-225: Mock setup ordering may cause flaky behavior.

The komgaMapper.getUnknownSeriesName() stub (line 225) is configured after the repository stubs that depend on its return value (lines 220-223). While Mockito typically handles this, it's clearer to set up the mapper stub first for readability and to match the execution order.

🔧 Suggested reordering
     `@Test`
     void shouldGetAllSeriesOptimized() {
         // Given: Mock the optimized repository method
         List<String> seriesNames = List.of("Series A", "Series B", "Series C");
+        when(komgaMapper.getUnknownSeriesName()).thenReturn("Unknown Series");
         when(bookRepository.findDistinctSeriesNamesGroupedByLibraryId(anyLong(), anyString()))
                 .thenReturn(seriesNames);
         
         // Mock books for the first page (Series A and Series B only)
         List<BookEntity> seriesABooks = List.of(seriesBooks.get(0), seriesBooks.get(1));
         List<BookEntity> seriesBBooks = List.of(seriesBooks.get(2), seriesBooks.get(3));
         
         when(bookRepository.findBooksBySeriesNameGroupedByLibraryId("Series A", 1L, "Unknown Series"))
                 .thenReturn(seriesABooks);
         when(bookRepository.findBooksBySeriesNameGroupedByLibraryId("Series B", 1L, "Unknown Series"))
                 .thenReturn(seriesBBooks);
         
-        when(komgaMapper.getUnknownSeriesName()).thenReturn("Unknown Series");
         when(komgaMapper.toKomgaSeriesDto(eq("Series A"), anyLong(), any()))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java`
around lines 210 - 225, In the shouldGetAllSeriesOptimized() test, move the
komgaMapper.getUnknownSeriesName() stub before any repository stubs that
reference its return value (e.g.,
bookRepository.findBooksBySeriesNameGroupedByLibraryId(...) and
bookRepository.findDistinctSeriesNamesGroupedByLibraryId(...)) so the mapper
value is defined first; update the setup order so
komgaMapper.getUnknownSeriesName() is stubbed before the two
findBooksBySeriesNameGroupedByLibraryId(...) when(...) calls to avoid potential
ordering confusion and make the test clearer.
booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java (1)

40-41: Use @DataJpaTest instead of @SpringBootTest to focus this test on the data layer.

This test validates repositories and entity mappings without depending on app-layer beans. The mocked TaskCronService and Flyway are never invoked—they exist only to silence missing dependency warnings. Replace @SpringBootTest(classes = BookloreApplication.class) with @DataJpaTest to load only JPA-related components, improving test speed and clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java`
around lines 40 - 41, Replace the class-level `@SpringBootTest`(classes =
BookloreApplication.class) on HibernateRegressionTest with `@DataJpaTest` so the
test loads only JPA components; remove or stop configuring mocks for app-layer
beans like TaskCronService and Flyway (they won’t be needed under `@DataJpaTest`),
ensure you import
org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest, and keep or
remove `@Transactional` as desired since `@DataJpaTest` is transactional by default.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java`:
- Around line 99-107: Equals/hashCode must use Hibernate.getClass to avoid
proxy/class mismatches and compute hashCode from the identifier rather than
getClass(); in BookLoreUserEntity replace the instanceof check with a class
equality using Hibernate.getClass(this) vs Hibernate.getClass(o) (e.g., if (o ==
null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false), cast
to BookLoreUserEntity and compare id as before, and change hashCode() to return
id == null ? 0 : id.hashCode(); apply same pattern to the other entities
(TagEntity, MoodEntity, CategoryEntity, LibraryEntity, AuthorEntity) to keep
behavior consistent.

In
`@booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java`:
- Around line 736-739: The test in HibernateRegressionTest currently asserts
that loaded.getMetadata().getSearchText() is not null even though searchText
isn't initialized in the fixture; update the test to avoid brittle default-value
assumptions by either initializing searchText on the test fixture (where
BookEntity / Metadata are created) or replacing the null-check assertion with a
non-throwing access assertion (e.g., assert that invoking
loaded.getMetadata().getSearchText() does not throw) so the test verifies
lazy-field accessibility rather than a specific default value.
- Around line 63-68: The test class HibernateRegressionTest uses field injection
for LibraryRepository and BookRepository; replace both `@Autowired` fields with
private final fields (libraryRepository, bookRepository), annotate the class
with Lombok's `@RequiredArgsConstructor`, remove the `@Autowired` annotations, and
update imports so Lombok's annotation is present; ensure constructor injection
semantics are preserved by keeping the field names and types unchanged so Spring
can autowire via the generated constructor.

---

Outside diff comments:
In
`@booklore-api/src/main/java/org/booklore/service/upload/FileUploadService.java`:
- Around line 63-95: Remove the method-level `@Transactional` on uploadFile so
filesystem operations (file.transferTo, moveFileToFinalLocation, metadata
extraction) are not executed inside a DB transaction; keep calling
auditService.log as-is (it already handles its own persistence/transaction), and
ensure uploadFile only handles filesystem work and throws/errors as before
without a surrounding transactional boundary.

---

Duplicate comments:
In `@booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java`:
- Around line 47-54: getAllBooksByLibraryIdsPaged currently pages via
bookRepository.findAllWithMetadataByLibraryIdsPage then filters the page content
with contentRestrictionService.applyRestrictions, but still returns a PageImpl
with the original page.getTotalElements(), producing incorrect totals; fix by
ensuring the returned PageImpl uses a total that reflects the filtered results
(either apply restrictions at query time in
bookRepository.findAllWithMetadataByLibraryIdsPage or compute the filtered total
before returning: e.g., run the same restriction logic against the full matching
set or query for a filtered count, then build PageImpl with the filtered list
and that corrected total), and keep mapping to DTOs via mapBookToDto as before
so the content matches the adjusted total.

In
`@booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java`:
- Around line 476-499: The post-import block using TransactionTemplate (newTx)
that calls metadataRefreshService.updateBookMetadata,
koboAutoShelfService.autoAddBookToKoboShelves and
notificationService.sendMessage(Topic.BOOK_ADD, fileProcessResult.getBook()) can
leave committed book/file records if it fails; either make the import +
enrichment atomic or add compensating cleanup and emit the event only after
enrichment commit. Fix by one of two approaches: (A) move the initial import and
this enrichment into a single transactional flow so the repository save and the
newTx work happen in one commit, or (B) on failure inside newTx catch/finally
perform compensating DB cleanup that deletes the created BookEntity and
associated BookFile records (using bookRepository and any file storage cleanup
used by performFileMove()), and only call notificationService.sendMessage after
successfully committing the newTx using the freshly loaded BookEntity/DTO (not
fileProcessResult.getBook()). Ensure the code references the
TransactionTemplate/newTx block, MetadataUpdateContext,
metadataRefreshService.updateBookMetadata,
koboAutoShelfService.autoAddBookToKoboShelves, and
notificationService.sendMessage so reviewers can locate and apply the change.

In
`@booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java`:
- Around line 912-923: The test currently asserts on entities while the
persistence context is still open, hiding lazy-init issues; after calling
bookRepository.findAllFullBooksBatch(...) in HibernateRegressionTest (the
findAllFullBooksBatch test), clear/detach the persistence context before
asserting (e.g., call EntityManager.clear() or detach the loaded BookEntity) so
that accessing lazy collections happens on a detached entity and will surface
lazy-init failures; reference the findAllFullBooksBatch call and the BookEntity
loaded variable when applying the detach/clear.

---

Nitpick comments:
In
`@booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java`:
- Around line 40-41: Replace the class-level `@SpringBootTest`(classes =
BookloreApplication.class) on HibernateRegressionTest with `@DataJpaTest` so the
test loads only JPA components; remove or stop configuring mocks for app-layer
beans like TaskCronService and Flyway (they won’t be needed under `@DataJpaTest`),
ensure you import
org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest, and keep or
remove `@Transactional` as desired since `@DataJpaTest` is transactional by default.

In `@booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java`:
- Around line 210-225: In the shouldGetAllSeriesOptimized() test, move the
komgaMapper.getUnknownSeriesName() stub before any repository stubs that
reference its return value (e.g.,
bookRepository.findBooksBySeriesNameGroupedByLibraryId(...) and
bookRepository.findDistinctSeriesNamesGroupedByLibraryId(...)) so the mapper
value is defined first; update the setup order so
komgaMapper.getUnknownSeriesName() is stubbed before the two
findBooksBySeriesNameGroupedByLibraryId(...) when(...) calls to avoid potential
ordering confusion and make the test clearer.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1df1b782-0316-4a94-92b7-583054a611d6

📥 Commits

Reviewing files that changed from the base of the PR and between fb5a28f and 2895451.

📒 Files selected for processing (62)
  • booklore-api/build.gradle.kts
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/LogoutService.java
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/controller/MetadataController.java
  • booklore-api/src/main/java/org/booklore/model/entity/AppSettingEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/AuthorEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookMetadataEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookdropFileEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/CategoryEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/ComicMetadataEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/KoboLibrarySnapshotEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/LibraryEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/MetadataFetchProposalEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/MoodEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/PdfAnnotationEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/TagEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/TaskHistoryEntity.java
  • booklore-api/src/main/java/org/booklore/repository/BookRepository.java
  • booklore-api/src/main/java/org/booklore/repository/projection/BookEmbeddingProjection.java
  • booklore-api/src/main/java/org/booklore/service/MagicShelfService.java
  • booklore-api/src/main/java/org/booklore/service/ReadingSessionService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
  • booklore-api/src/main/java/org/booklore/service/book/NotebookService.java
  • booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java
  • booklore-api/src/main/java/org/booklore/service/komga/KomgaService.java
  • booklore-api/src/main/java/org/booklore/service/library/BookRestorationService.java
  • booklore-api/src/main/java/org/booklore/service/library/LibraryProcessingService.java
  • booklore-api/src/main/java/org/booklore/service/library/LibraryService.java
  • booklore-api/src/main/java/org/booklore/service/metadata/BookMetadataService.java
  • booklore-api/src/main/java/org/booklore/service/metadata/MetadataTaskService.java
  • booklore-api/src/main/java/org/booklore/service/progress/ReadingProgressService.java
  • booklore-api/src/main/java/org/booklore/service/recommender/BookRecommendationService.java
  • booklore-api/src/main/java/org/booklore/service/restriction/ContentRestrictionService.java
  • booklore-api/src/main/java/org/booklore/service/upload/FileUploadService.java
  • booklore-api/src/main/java/org/booklore/service/watcher/BookFilePersistenceService.java
  • booklore-api/src/main/java/org/booklore/service/watcher/LibraryFileEventProcessor.java
  • booklore-api/src/main/java/org/booklore/task/tasks/BookRecommendationUpdaterTask.java
  • booklore-api/src/main/resources/application.yaml
  • booklore-api/src/test/java/org/booklore/config/security/service/LogoutServiceTest.java
  • booklore-api/src/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorEdgeCasesTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorFieldCoverageTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorNumericFieldsTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorServiceIntegrationTest.java
  • booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java
  • booklore-api/src/test/java/org/booklore/service/book/BookCreatorServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/book/BookFileAttachmentServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/book/BookFileDetachmentServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/bookdrop/BookDropServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/kobo/KoboLibrarySnapshotServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceRegressionTest.java
  • booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/watcher/LibraryFileEventProcessorTest.java
  • booklore-api/src/test/java/org/booklore/task/tasks/BookRecommendationUpdaterTaskTest.java
  • booklore-api/src/test/resources/application-test.yml
💤 Files with no reviewable changes (2)
  • booklore-api/src/main/java/org/booklore/controller/MetadataController.java
  • booklore-api/src/main/java/org/booklore/service/metadata/BookMetadataService.java
✅ Files skipped from review due to trivial changes (8)
  • booklore-api/src/test/resources/application-test.yml
  • booklore-api/src/main/java/org/booklore/repository/projection/BookEmbeddingProjection.java
  • booklore-api/src/main/java/org/booklore/model/entity/CategoryEntity.java
  • booklore-api/src/test/java/org/booklore/service/book/BookFileAttachmentServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorEdgeCasesTest.java
  • booklore-api/src/test/java/org/booklore/service/kobo/KoboLibrarySnapshotServiceTest.java
  • booklore-api/src/main/java/org/booklore/service/ReadingSessionService.java
  • booklore-api/src/main/java/org/booklore/service/recommender/BookRecommendationService.java
🚧 Files skipped from review as they are similar to previous changes (30)
  • booklore-api/src/test/java/org/booklore/service/watcher/LibraryFileEventProcessorTest.java
  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
  • booklore-api/src/main/java/org/booklore/model/entity/AppSettingEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookEntity.java
  • booklore-api/src/main/java/org/booklore/config/security/service/LogoutService.java
  • booklore-api/src/main/java/org/booklore/model/entity/TaskHistoryEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/TagEntity.java
  • booklore-api/src/main/java/org/booklore/service/watcher/BookFilePersistenceService.java
  • booklore-api/src/main/java/org/booklore/service/watcher/LibraryFileEventProcessor.java
  • booklore-api/src/main/java/org/booklore/service/progress/ReadingProgressService.java
  • booklore-api/src/main/java/org/booklore/service/MagicShelfService.java
  • booklore-api/src/main/java/org/booklore/service/restriction/ContentRestrictionService.java
  • booklore-api/src/main/java/org/booklore/model/entity/MetadataFetchProposalEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/PdfAnnotationEntity.java
  • booklore-api/src/main/java/org/booklore/service/library/BookRestorationService.java
  • booklore-api/src/test/java/org/booklore/task/tasks/BookRecommendationUpdaterTaskTest.java
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/main/java/org/booklore/model/entity/AuthorEntity.java
  • booklore-api/src/test/java/org/booklore/service/book/BookFileDetachmentServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorServiceIntegrationTest.java
  • booklore-api/src/main/java/org/booklore/service/book/NotebookService.java
  • booklore-api/src/test/java/org/booklore/service/bookdrop/BookDropServiceTest.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookdropFileEntity.java
  • booklore-api/src/main/java/org/booklore/task/tasks/BookRecommendationUpdaterTask.java
  • booklore-api/src/main/java/org/booklore/model/entity/LibraryEntity.java
  • booklore-api/src/main/java/org/booklore/service/komga/KomgaService.java
  • booklore-api/src/main/java/org/booklore/service/library/LibraryService.java
  • booklore-api/src/main/java/org/booklore/model/entity/KoboLibrarySnapshotEntity.java
  • booklore-api/src/main/java/org/booklore/service/metadata/MetadataTaskService.java
  • booklore-api/src/test/java/org/booklore/service/book/BookCreatorServiceTest.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Analyze (java-kotlin)
🧰 Additional context used
📓 Path-based instructions (2)
booklore-api/src/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

booklore-api/src/**/*.java: Use 4-space indentation 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
Keep JPA entities on the *Entity suffix

Files:

  • booklore-api/src/test/java/org/booklore/config/security/service/LogoutServiceTest.java
  • booklore-api/src/main/java/org/booklore/service/upload/FileUploadService.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java
  • booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceRegressionTest.java
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/model/entity/ComicMetadataEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookMetadataEntity.java
  • booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java
  • booklore-api/src/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorFieldCoverageTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorNumericFieldsTest.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/model/entity/MoodEntity.java
  • booklore-api/src/main/java/org/booklore/service/library/LibraryProcessingService.java
  • booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceTest.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
  • booklore-api/src/main/java/org/booklore/repository/BookRepository.java
  • booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java
  • booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java
booklore-api/src/test/**/*.java

📄 CodeRabbit inference engine (AGENTS.md)

Prefer focused unit tests; use @SpringBootTest only when the Spring context is required

Files:

  • booklore-api/src/test/java/org/booklore/config/security/service/LogoutServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceRegressionTest.java
  • booklore-api/src/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorFieldCoverageTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorNumericFieldsTest.java
  • booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java
  • booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java
🧠 Learnings (7)
📚 Learning: 2026-03-25T16:46:06.366Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-25T16:46:06.366Z
Learning: Applies to booklore-api/src/**/*.java : Keep JPA entities on the *Entity suffix

Applied to files:

  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java
  • booklore-api/src/main/java/org/booklore/model/entity/ComicMetadataEntity.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookMetadataEntity.java
  • booklore-api/src/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorFieldCoverageTest.java
  • booklore-api/src/main/java/org/booklore/model/entity/MoodEntity.java
  • booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java
📚 Learning: 2026-03-25T16:46:06.366Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-25T16:46:06.366Z
Learning: Applies to booklore-api/src/**/*.java : Use MapStruct for entity/DTO mapping

Applied to files:

  • booklore-api/src/main/java/org/booklore/model/entity/BookMetadataEntity.java
  • booklore-api/src/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
  • booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java
  • booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java
📚 Learning: 2026-03-25T16:46:06.366Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-25T16:46:06.366Z
Learning: Applies to booklore-api/src/test/**/*.java : Prefer focused unit tests; use SpringBootTest only when the Spring context is required

Applied to files:

  • booklore-api/src/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorFieldCoverageTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorNumericFieldsTest.java
  • booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceTest.java
  • booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java
  • booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java
📚 Learning: 2026-03-25T16:46:06.366Z
Learnt from: CR
Repo: grimmory-tools/grimmory PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-25T16:46:06.366Z
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/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorFieldCoverageTest.java
  • booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorNumericFieldsTest.java
  • booklore-api/src/test/java/org/booklore/service/HibernateRegressionTest.java
📚 Learning: 2026-03-24T18:58:08.199Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:291-294
Timestamp: 2026-03-24T18:58:08.199Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a Hardcover edition has no page count, falling back to the book-level page count (book.pages) is the preferred behavior. Skipping the sync entirely in this case is considered worse than using a slightly approximate total, because the page count is only a proxy for converting progress % to page numbers for the API. The Hardcover API allows setting page-based progress even when the edition lacks a page count, and progress displays in at least some UI views (e.g., the book detail dropdown).

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
📚 Learning: 2026-03-25T21:02:55.427Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 195
File: booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java:15-19
Timestamp: 2026-03-25T21:02:55.427Z
Learning: In `booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java` and `booklore-api/src/main/java/org/booklore/config/logging/filter/RequestLoggingFilter.java`, logging all request headers and payloads at DEBUG level is intentional and accepted behavior in this project. The previous `LoggingFilter` also logged all headers. No header redaction or payload scrubbing is required — the DEBUG log level is considered sufficient access control. The new filter is not profile-gated (unlike the old `Profile("dev")` filter) but relies on `logger.isDebugEnabled()` for gating.

Applied to files:

  • booklore-api/src/main/resources/application.yaml
🔇 Additional comments (34)
booklore-api/src/test/java/org/booklore/config/security/service/LogoutServiceTest.java (1)

152-152: Bulk-save verification looks correct.

This assertion is aligned with the service’s batched token persistence path and keeps the test intent clear.

booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java (1)

10-10: Objects import is correct and needed.

Used by Objects.equals(...) in equals, so this addition looks good.

booklore-api/build.gradle.kts (2)

141-146: LGTM — consider documenting the heap size rationale.

The 2GB heap allocation is reasonable for JPA integration tests with lazy loading verification, especially when combined with the bytecode enhancement agent (-XX:+EnableDynamicAgentLoading). If this was added to address OOM during tests, a brief comment documenting why would help future maintainers.


134-139: No changes required—Hibernate enhancement configuration is properly implemented with correct transactional boundaries.

The concerns raised in the original review have been addressed in the codebase:

  1. Bidirectional association synchronization: Service code correctly sets both sides of relationships. For example, in LibraryService.java (line 142), new LibraryPathEntity objects are built with .library(library) back-reference before being added to the collection. The HibernateRegressionTest.LibraryPathBackReference class explicitly validates this pattern works correctly.

  2. Transactional coverage for lazy loading: LibraryService has class-level @Transactional(readOnly = true) covering all methods, with explicit @Transactional decorators on write operations. Similar patterns apply across other services. The absence of hibernate.open-in-view is intentional—it enforces that lazy fields are accessed only within active transaction boundaries, which is the correct approach.

  3. Regression testing: The HibernateRegressionTest suite includes multiple @Nested test classes specifically validating entity relationships (LibraryPathBackReference), lazy loading (CollectionInitialization), and lazy field access patterns (MetadataDescriptionAccess) after the Hibernate refactoring.

booklore-api/src/main/java/org/booklore/service/book/BookService.java (2)

50-51: Class-level @Transactional holds DB connections during file I/O operations.

The class-level @Transactional(readOnly = true) wraps file-serving methods like downloadBook, downloadAllBookFiles, getBookContent, and streamBookContent. These methods spend most of their time in filesystem/HTTP I/O, so this keeps a Hibernate session and DB connection open for the entire transfer duration rather than just the initial lookup.

Consider removing the class-level annotation and adding @Transactional(readOnly = true) only to the specific database-access methods that need it.


104-133: LGTM! New paginated endpoint follows existing patterns.

The getBookDTOsPaged method correctly:

  • Performs authorization checks (admin vs. user library scope)
  • Batch-loads progress data to avoid N+1 queries
  • Filters shelves by user ID
  • Returns a properly mapped Page<Book>
booklore-api/src/main/java/org/booklore/model/entity/ComicMetadataEntity.java (1)

82-107: LGTM! Good optimization from SUBSELECT to BatchSize.

Replacing @Fetch(FetchMode.SUBSELECT) with @BatchSize(size = 20) on the collection associations is a sensible optimization. SUBSELECT can load all collections for all loaded parent entities in one query (memory-heavy for large datasets), while BatchSize provides more predictable memory usage by loading collections in configurable batches.

booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorNumericFieldsTest.java (1)

49-60: LGTM! Test configuration aligns with application-test.yml baseline.

The addition of provider_disables_autocommit=false is correct for H2-based tests. This differs from production's true setting intentionally—H2 doesn't benefit from the HikariCP optimization, and false ensures proper test transaction management with @Transactional rollback semantics.

booklore-api/src/test/java/org/booklore/service/BookRuleEvaluatorFieldCoverageTest.java (1)

56-67: LGTM! Consistent with other test files in this PR.

The configuration changes mirror those in the sibling test classes, maintaining consistency across the test suite.

booklore-api/src/main/resources/application.yaml (2)

60-68: Good HikariCP and OSIV optimizations.

  • auto-commit: false paired with provider_disables_autocommit: true eliminates unnecessary JDBC roundtrips for autocommit status checks.
  • open-in-view: false is a best practice that prevents accidental lazy loading in controller/view layers, forcing proper eager fetching in the service layer.

79-79: Good safety net for pagination queries.

Enabling fail_on_pagination_over_collection_fetch: true will cause Hibernate to fail fast when attempting in-memory pagination over collection fetches (which silently loads all rows then paginates in memory). This helps catch performance issues during development rather than in production.

Ensure all paginated repository queries use only @EntityGraph with ToOne paths or rely on @BatchSize for collections. The repository changes in this PR appear to follow this pattern correctly.

booklore-api/src/main/java/org/booklore/controller/BookController.java (1)

77-83: LGTM! New paginated endpoint follows existing authorization patterns.

The endpoint correctly:

  • Uses Spring Data's Pageable argument resolver (enabled via @EnableSpringDataWebSupport)
  • Respects the global max-page-size: 100 limit configured in application.yaml
  • Delegates authorization to the service layer (consistent with the existing GET /api/v1/books endpoint)
  • Hides the Pageable parameter from OpenAPI docs (standard practice since pagination params are auto-documented)
booklore-api/src/test/java/org/booklore/repository/BookOpdsRepositoryDataJpaTest.java (1)

48-65: LGTM! Consistent test configuration updates.

The changes align with the pattern applied across all test files in this PR.

booklore-api/src/main/java/org/booklore/repository/BookRepository.java (5)

69-72: LGTM! Correctly narrowed EntityGraph for list queries.

Limiting @EntityGraph to ToOne paths (metadata, metadata.comicMetadata, library) while relying on @BatchSize for collections prevents Cartesian products and aligns with the fail_on_pagination_over_collection_fetch: true configuration.


96-107: LGTM! New rescan and soft-delete queries.

  • findAllByLibraryIdForRescan: Appropriately fetches only bookFiles and libraryPath needed for rescan logic.
  • findDeletedByLibraryIdWithFiles: Correctly scopes to soft-deleted books with necessary file paths for restoration.

336-351: LGTM! Cleaner series matching with parameterized condition.

The change from file-based fallback to (m.seriesName IS NULL AND :seriesName = :unknownSeriesName) is more efficient and easier to understand. It correctly matches books without a series name only when the requested series name equals the "Unknown Series" placeholder.


431-491: LGTM! Pagination queries follow ToOne-only EntityGraph pattern.

All new paginated methods (findAllWithMetadataPaged, findAllWithMetadataByLibraryIdPaged, findAllWithMetadataPage, findAllWithMetadataByLibraryIdsPage, findAllFullBooksBatch) correctly use ToOne-only paths in their @EntityGraph definitions, avoiding Cartesian products and ensuring compatibility with fail_on_pagination_over_collection_fetch: true.


497-505: LGTM! Lightweight projection for recommendation lookups.

The findAllEmbeddingsForRecommendation method uses a projection interface to fetch only the required fields (bookId, embeddingVector, seriesName), which is significantly more efficient than loading full entities when pre-computed embeddings are available.

booklore-api/src/main/java/org/booklore/model/entity/BookMetadataEntity.java (2)

303-314: Verify Hibernate bytecode enhancement is configured for lazy basic fields.

@Basic(fetch = FetchType.LAZY) with @LazyGroup requires Hibernate bytecode enhancement with enableLazyInitialization = true. Without this, the lazy annotation will be silently ignored and these fields will still load eagerly.

#!/bin/bash
# Check if Hibernate enhancement includes enableLazyInitialization
fd -HI 'build\.gradle\.kts' | xargs grep -A 10 'hibernate' | grep -E '(enableLazyInitialization|enableAssociationManagement|enhancement)'

379-417: LGTM - Batch loading replaces subselect fetching.

Replacing @Fetch(FetchMode.SUBSELECT) with @BatchSize(size = 20) and adding @Builder.Default initializers is a sound optimization. Batch loading provides more predictable query patterns and the non-null defaults prevent NPEs when building entities.

booklore-api/src/main/java/org/booklore/app/service/AppSeriesService.java (1)

175-182: LGTM - Query optimization aligns with batch loading strategy.

The query now fetches only the To-One metadata association eagerly. The authors and bookFiles collections accessed later (lines 207-212, 220-228) will be batch-loaded via @BatchSize(size = 20) on BookMetadataEntity.authors and BookEntity.bookFiles. This reduces the initial query complexity while still avoiding N+1 when iterating over multiple books.

booklore-api/src/test/java/org/booklore/service/komga/KomgaServiceTest.java (1)

105-109: LGTM - Test stubs align with refactored service flow.

The stubs correctly match the new repository method signatures for series-grouped queries. The getUnknownSeriesName() mock returns a consistent value, which aligns with how the production code uses it across multiple calls.

booklore-api/src/main/java/org/booklore/service/library/LibraryProcessingService.java (3)

52-71: LGTM - Repository-based book fetching improves memory efficiency.

Moving from libraryEntity.getBookEntities() to bookRepository.findAllByLibraryIdForRescan(libraryId) avoids eagerly loading all books through the entity relationship, which is especially beneficial for large libraries.


73-132: LGTM - Proper entity refresh after persistence context clear.

The pattern of calling entityManager.clear() followed by re-fetching both the library (via findByIdWithPaths) and books (via findAllByLibraryIdForRescan) ensures fresh state for the subsequent detectNewBookPaths call. This correctly handles detached entity scenarios.


148-164: LGTM - Static method improves testability.

Making detectDeletedBookIds a protected static method that accepts a List<BookEntity> directly allows for easier unit testing without requiring entity relationships to be set up.

booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java (3)

56-65: LGTM - Explicit initialization prevents lazy loading issues outside transaction.

Calling Hibernate.initialize() on authors and categories ensures these collections are loaded within the transaction boundary, making the entities safe to use after the transaction completes.


88-100: LGTM - Conditional update avoids unnecessary writes.

The Objects.equals check before setting embeddingVector prevents dirty-checking overhead and unnecessary database updates when embeddings haven't changed.


102-116: LGTM - Batch processing with reasonable chunk size.

Processing recommendations in batches with findAllById and saveAll per batch prevents loading the entire dataset into memory and allows Hibernate to flush periodically.

booklore-api/src/main/java/org/booklore/model/entity/MoodEntity.java (1)

27-30: LGTM - Consistent batch size configuration.

Adding @BatchSize(size = 20) to the inverse side of the many-to-many relationship maintains consistency with the owning side in BookMetadataEntity.moods and other collections in this PR.

booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceRegressionTest.java (2)

56-70: LGTM - Constructor updated to match production service.

The test correctly injects the new BookRepository dependency, maintaining alignment with the production LibraryProcessingService constructor.


95-96: LGTM - Stubs updated for refactored repository methods.

The test now stubs findByIdWithPaths and findAllByLibraryIdForRescan to match the production rescan flow that separates library path loading from book fetching.

booklore-api/src/test/java/org/booklore/service/library/LibraryProcessingServiceTest.java (2)

59-73: LGTM - Test setup correctly updated for new dependency.

The constructor injection of BookRepository mock aligns with the production service changes, maintaining proper test isolation.


567-584: LGTM - Verification confirms proper entity refresh pattern.

The times(2) verification for both findByIdWithPaths and findAllByLibraryIdForRescan correctly validates that the service re-fetches state after entityManager.clear(), ensuring no stale entity references are used.

booklore-api/src/main/java/org/booklore/service/bookdrop/BookDropService.java (1)

43-45: Constructor-injected transaction manager matches the existing service pattern.

Adding PlatformTransactionManager as a final dependency keeps this service on the Lombok constructor-injection style and avoids field injection. As per coding guidelines: "Prefer constructor injection via Lombok patterns already used in the codebase. Do not introduce @Autowired field injection".

Also applies to: 81-81

Comment thread booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java Outdated
@balazs-szucs balazs-szucs requested a review from zachyale March 30, 2026 15:31
# Conflicts:
#	booklore-api/src/main/java/org/booklore/service/book/BookService.java
#	frontend/src/app/features/book/service/book.service.ts
@imajes

imajes commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai: review

@coderabbitai

coderabbitai Bot commented Mar 31, 2026

Copy link
Copy Markdown
Contributor

@imajes Sure, I'll review the PR now!

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@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.

Actionable comments posted: 6

♻️ Duplicate comments (1)
booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java (1)

47-53: ⚠️ Potential issue | 🟠 Major

Post-filtering breaks page totals and offsets.

applyRestrictions(...) can remove books on Line 49, but Line 53 still reuses the unfiltered page.getTotalElements(). That makes totalElements/totalPages inaccurate, and because the filtering happens after the offset is chosen, clients can still get sparse or empty pages. This needs query-level restriction handling, or an equivalent restricted count/slice calculation before building the PageImpl.

🤖 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/book/BookQueryService.java`
around lines 47 - 53, The current getAllBooksByLibraryIdsPaged applies
contentRestrictionService.applyRestrictions after executing
bookRepository.findAllWithMetadataByLibraryIdsPage, causing incorrect totals and
sparse pages; change it to compute restriction-aware pagination: use
bookRepository to fetch only ids for the libraryIds (e.g., findIdsByLibraryIds),
apply contentRestrictionService.applyRestrictions to those entities/ids to get
the filtered id set, calculate totalFiltered = filteredIds.size(), compute the
sublist of IDs for the requested Pageable (offset/limit), then fetch the full
BookEntity page for that sublist (e.g., findAllWithMetadataByIds) and map them
with mapBookToDto before returning new PageImpl<>(dtos, pageable,
totalFiltered); ensure getAllBooksByLibraryIdsPaged,
bookRepository.findAllWithMetadataByLibraryIdsPage,
contentRestrictionService.applyRestrictions, and mapBookToDto are used/updated
accordingly.
🧹 Nitpick comments (2)
booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java (1)

87-96: Consider batching session and token saves.

The loop calls save() individually for each session (line 89) and each refresh token (line 95). Given the PR objectives mention optimizing token revocation with saveAll, consider collecting entities and persisting in batch:

♻️ Suggested batched approach
+       List<RefreshTokenEntity> tokensToRevoke = new ArrayList<>();
        for (OidcSessionEntity session : sessions) {
            session.setRevoked(true);
-           oidcSessionRepository.save(session);

            var user = session.getUser();
            refreshTokenRepository.findAllByUserAndRevokedFalse(user).forEach(token -> {
                token.setRevoked(true);
                token.setRevocationDate(Instant.now());
-               refreshTokenRepository.save(token);
+               tokensToRevoke.add(token);
            });
            // ... notifications and audit ...
        }
+       oidcSessionRepository.saveAll(sessions);
+       refreshTokenRepository.saveAll(tokensToRevoke);

,

🤖 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/config/security/oidc/BackchannelLogoutService.java`
around lines 87 - 96, In BackchannelLogoutService (the loop iterating over
OidcSessionEntity sessions), avoid per-entity saves by collecting modified
OidcSessionEntity objects into a list and calling
oidcSessionRepository.saveAll(collectedSessions) once, and similarly collect
modified RefreshToken entities (after setting revoked=true and
revocationDate=Instant.now()) into a list to call
refreshTokenRepository.saveAll(collectedTokens) once; ensure you still set
session.setRevoked(true) and token.setRevoked(true)/setRevocationDate(...)
before batching, and keep the method transactional so both batched saves occur
atomically.
booklore-api/src/main/java/org/booklore/service/book/BookService.java (1)

123-131: Consider extracting common enrichment logic.

The book enrichment and shelf filtering logic (lines 123-131) duplicates code from getBookDTOs (lines 92-100). A private helper method could reduce this duplication:

private void enrichBooksForUser(Collection<Book> books, Long userId, 
        Map<Long, UserBookProgressEntity> progressMap,
        Map<Long, UserBookFileProgressEntity> fileProgressMap,
        boolean setShelvesNullWhenEmpty) {
    books.forEach(book -> {
        readingProgressService.enrichBookWithProgress(
                book, progressMap.get(book.getId()), fileProgressMap.get(book.getId()));
        Set<Shelf> filtered = filterShelvesByUserId(book.getShelves(), userId);
        book.setShelves(setShelvesNullWhenEmpty && filtered != null && filtered.isEmpty() 
                ? null : filtered);
    });
}
🤖 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/book/BookService.java` around
lines 123 - 131, Duplicate enrichment and shelf-filtering logic in the bookPage
loop and getBookDTOs should be extracted into a private helper (e.g.,
enrichBooksForUser) that accepts Collection<Book> books, Long userId,
Map<Long,UserBookProgressEntity> progressMap,
Map<Long,UserBookFileProgressEntity> fileProgressMap, and a boolean
setShelvesNullWhenEmpty; move the
readingProgressService.enrichBookWithProgress(...) call and the
filterShelvesByUserId(...) + book.setShelves(...) logic into that helper and
replace both original blocks (the bookPage.forEach and the getBookDTOs loop)
with calls to enrichBooksForUser to eliminate duplication while preserving the
current null/empty shelf behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java`:
- Around line 32-35: The PROCESSED_JTIS Caffeine cache in
BackchannelLogoutService currently has maximumSize(1_000), which can be
exhausted and allow JTI replay; change the cache configuration in
BackchannelLogoutService.PROCESSED_JTIS to use a larger maximumSize (e.g.,
10_000) or implement rate limiting on the /backchannel-logout endpoint (see
OidcAuthController) to prevent cache eviction attacks—locate the PROCESSED_JTIS
declaration and either update maximumSize(10_000) or wire in a rate limiter on
OidcAuthController.backchannelLogout to throttle requests and protect the cache.

In
`@booklore-api/src/main/java/org/booklore/config/security/service/AuthRateLimitService.java`:
- Around line 23-29: The rate-limit cache in AuthRateLimitService is too small
(maximumSize(1000)) and can be evicted by distributed attacks; update the
attemptCache configuration in the AuthRateLimitService constructor to use a
larger capacity (e.g., maximumSize(10000)) or replace the in-memory Caffeine
cache with a robust external store (e.g., Redis with TTL) for tracking keys like
the "login:ip:" / "login:user:" and "refresh:" entries so counters are not lost
under churn; modify the attemptCache initialization accordingly (or inject a
Redis-backed store) and ensure other methods that read/write attemptCache still
operate against the new store.

In
`@booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java`:
- Around line 220-227: The JPQL IN :libraryIds will fail for null/empty; update
handling so getContinueReading()/getContinueListening() guard libraryIds from
AppBookService.getAccessibleLibraryIds(): if libraryIds is empty return an empty
result immediately, and if libraryIds is null allow "no filter" by changing the
repository queries (findTopContinueReadingBookIds and
findTopContinueListeningBookIds) to use a null-safe predicate like "AND
(:libraryIds IS NULL OR b.library.id IN :libraryIds)" so null means no library
restriction while empty collection still yields no results.
- Around line 212-223: The query in findTopContinueReadingBookIds and
findTopContinueListeningBookIds uses SELECT DISTINCT with ORDER BY
ubp.lastReadTime while joining b.bookFiles, which causes PostgreSQL errors and
inefficiency; replace the JOIN b.bookFiles bf + DISTINCT with an EXISTS subquery
that checks for a matching BookFile (e.g., EXISTS (SELECT 1 FROM BookFileEntity
bf WHERE bf.book = b AND bf.isBookFormat = true AND bf.bookType <>
org.booklore.model.enums.BookFileType.AUDIOBOOK)), remove DISTINCT from the
select, keep SELECT ubp.book.id and the ORDER BY ubp.lastReadTime DESC, and
preserve the other filters (ubp.user.id, b.deleted, b.library.id IN :libraryIds,
ubp.lastReadTime IS NOT NULL) so the result is deduplicated correctly and orders
by lastReadTime.

In `@booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java`:
- Around line 102-106: Reject non-positive batchSize at the start of
saveRecommendationsInBatches by validating the batchSize parameter (and
optionally recommendations) before entering the loop; if batchSize <= 0, throw
an IllegalArgumentException with a clear message so the for-loop that uses
batchSize can never hang or misbehave.

In `@frontend/src/app/features/book/service/book.service.ts`:
- Line 105: The staleTime of 30_000 combined with the current cache patch in
updateBookShelves risks showing stale list entries; update updateBookShelves to
invalidate or refetch the BOOKS_QUERY_KEY list after the mutation completes (or
on mutation success) instead of relying solely on the patch — e.g., perform your
optimistic cache patch as now but call
queryClient.invalidateQueries(BOOKS_QUERY_KEY) (or trigger a refetch) on success
so backend-side changes beyond the patched fields are reflected immediately.

---

Duplicate comments:
In `@booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java`:
- Around line 47-53: The current getAllBooksByLibraryIdsPaged applies
contentRestrictionService.applyRestrictions after executing
bookRepository.findAllWithMetadataByLibraryIdsPage, causing incorrect totals and
sparse pages; change it to compute restriction-aware pagination: use
bookRepository to fetch only ids for the libraryIds (e.g., findIdsByLibraryIds),
apply contentRestrictionService.applyRestrictions to those entities/ids to get
the filtered id set, calculate totalFiltered = filteredIds.size(), compute the
sublist of IDs for the requested Pageable (offset/limit), then fetch the full
BookEntity page for that sublist (e.g., findAllWithMetadataByIds) and map them
with mapBookToDto before returning new PageImpl<>(dtos, pageable,
totalFiltered); ensure getAllBooksByLibraryIdsPaged,
bookRepository.findAllWithMetadataByLibraryIdsPage,
contentRestrictionService.applyRestrictions, and mapBookToDto are used/updated
accordingly.

---

Nitpick comments:
In
`@booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java`:
- Around line 87-96: In BackchannelLogoutService (the loop iterating over
OidcSessionEntity sessions), avoid per-entity saves by collecting modified
OidcSessionEntity objects into a list and calling
oidcSessionRepository.saveAll(collectedSessions) once, and similarly collect
modified RefreshToken entities (after setting revoked=true and
revocationDate=Instant.now()) into a list to call
refreshTokenRepository.saveAll(collectedTokens) once; ensure you still set
session.setRevoked(true) and token.setRevoked(true)/setRevocationDate(...)
before batching, and keep the method transactional so both batched saves occur
atomically.

In `@booklore-api/src/main/java/org/booklore/service/book/BookService.java`:
- Around line 123-131: Duplicate enrichment and shelf-filtering logic in the
bookPage loop and getBookDTOs should be extracted into a private helper (e.g.,
enrichBooksForUser) that accepts Collection<Book> books, Long userId,
Map<Long,UserBookProgressEntity> progressMap,
Map<Long,UserBookFileProgressEntity> fileProgressMap, and a boolean
setShelvesNullWhenEmpty; move the
readingProgressService.enrichBookWithProgress(...) call and the
filterShelvesByUserId(...) + book.setShelves(...) logic into that helper and
replace both original blocks (the bookPage.forEach and the getBookDTOs loop)
with calls to enrichBooksForUser to eliminate duplication while preserving the
current null/empty shelf behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b6df7c7a-0e25-4ea2-8ce0-a5bd0126c9ab

📥 Commits

Reviewing files that changed from the base of the PR and between b219c0f and fd48901.

📒 Files selected for processing (20)
  • Dockerfile
  • booklore-api/build.gradle.kts
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java
  • booklore-api/src/main/java/org/booklore/config/security/oidc/OidcStateService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/AuthRateLimitService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/LoginRateLimitService.java
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/mapper/v2/BookMapperV2.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java
  • booklore-api/src/main/java/org/booklore/service/IconService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/metadata/BookMetadataService.java
  • booklore-api/src/main/java/org/booklore/util/koreader/EpubCfiService.java
  • booklore-api/src/main/resources/application.yaml
  • booklore-api/src/test/java/org/booklore/service/book/BookCreatorServiceTest.java
  • dev.docker-compose.yml
  • frontend/src/app/features/book/service/book.service.ts
💤 Files with no reviewable changes (1)
  • booklore-api/src/main/java/org/booklore/service/metadata/BookMetadataService.java
✅ Files skipped from review due to trivial changes (2)
  • booklore-api/src/main/java/org/booklore/config/security/oidc/OidcStateService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/LoginRateLimitService.java
🚧 Files skipped from review as they are similar to previous changes (6)
  • booklore-api/build.gradle.kts
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/test/java/org/booklore/service/book/BookCreatorServiceTest.java
  • booklore-api/src/main/resources/application.yaml
📜 Review details
🧰 Additional context used
📓 Path-based instructions (3)
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/IconService.java
  • booklore-api/src/main/java/org/booklore/util/koreader/EpubCfiService.java
  • booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/AuthRateLimitService.java
  • booklore-api/src/main/java/org/booklore/mapper/v2/BookMapperV2.java
  • booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
frontend/src/**/*.{ts,tsx,html,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation in TypeScript, HTML, and SCSS files

Files:

  • frontend/src/app/features/book/service/book.service.ts
frontend/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

frontend/src/app/**/*.{ts,tsx}: Prefer inject() over constructor injection
Follow frontend/eslint.config.js: component selectors use app-*, directive selectors use app*, and any is disallowed

Files:

  • frontend/src/app/features/book/service/book.service.ts
🧠 Learnings (3)
📚 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 : Use MapStruct for entity/DTO mapping

Applied to files:

  • booklore-api/src/main/java/org/booklore/mapper/v2/BookMapperV2.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
📚 Learning: 2026-03-24T18:58:08.199Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:291-294
Timestamp: 2026-03-24T18:58:08.199Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a Hardcover edition has no page count, falling back to the book-level page count (book.pages) is the preferred behavior. Skipping the sync entirely in this case is considered worse than using a slightly approximate total, because the page count is only a proxy for converting progress % to page numbers for the API. The Hardcover API allows setting page-based progress even when the edition lacks a page count, and progress displays in at least some UI views (e.g., the book detail dropdown).

Applied to files:

  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.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/book/BookService.java
🔇 Additional comments (9)
booklore-api/src/main/java/org/booklore/service/IconService.java (1)

41-41: Looks good — cache size reduction is consistent with current eviction flow.

Line 41 integrates cleanly with updateCache(...) and does not introduce a functional regression.

dev.docker-compose.yml (1)

20-20: JAVA_TOOL_OPTIONS parity is preserved.

This override includes the full JVM flag set expected by the runtime configuration, including the newly added tuning flags.

Dockerfile (1)

71-78: Runtime JVM tuning additions look good.

The extended JAVA_TOOL_OPTIONS block is coherent and keeps -XX:+ExitOnOutOfMemoryError while adding the intended memory/compiler/Shenandoah tuning parameters.

booklore-api/src/main/java/org/booklore/service/book/BookService.java (4)

50-51: Class-level transaction still holds connections during file I/O.

This concern was raised in a previous review and remains unaddressed. Methods like downloadBook, downloadAllBookFiles, getBookContent, and streamBookContent perform long-running file/HTTP I/O operations but will now hold a Hibernate session and DB connection for the entire transfer duration.

Consider moving @Transactional(readOnly = true) to individual database-access methods instead, leaving file-serving methods non-transactional.


30-31: LGTM!

Standard Spring Data imports for pagination support.


105-134: LGTM! Authorization checks are correctly implemented.

The method properly:

  • Authenticates the user via authenticationService.getAuthenticatedUser()
  • Restricts non-admin users to only their assigned libraries
  • Enriches books with user-specific progress data
  • Filters shelves to the current user

The pattern mirrors the existing getBookDTOs method appropriately.


260-263: LGTM!

Correctly overrides the class-level readOnly = true for this write operation.

booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java (1)

9-18: No issues in the added imports.

Added Pageable and Collection are consistent with the new repository method signatures.

booklore-api/src/main/java/org/booklore/mapper/v2/BookMapperV2.java (1)

31-37: Keep the description inclusion rule in one place.

Leaving description out of mapMetadata(...) and letting BookQueryService.mapBookToDto(...) opt back in only when requested keeps that policy centralized instead of splitting it across two mappings.

Comment thread frontend/src/app/features/book/service/book.service.ts

@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.

Actionable comments posted: 2

♻️ Duplicate comments (4)
booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java (1)

212-244: ⚠️ Potential issue | 🟠 Major

Refactor both “top continue” queries to avoid DISTINCT over JOIN b.bookFiles.

Line 212 and Line 233 still rely on SELECT DISTINCT ubp.book.id ... ORDER BY ubp.lastReadTime DESC after joining b.bookFiles, which remains a fragile pattern and was already flagged earlier.

Suggested JPQL rewrite (apply to both methods)
-            SELECT DISTINCT ubp.book.id FROM UserBookProgressEntity ubp
+            SELECT ubp.book.id FROM UserBookProgressEntity ubp
             JOIN ubp.book b
-            JOIN b.bookFiles bf
             WHERE ubp.user.id = :userId
               AND ubp.readStatus IN (org.booklore.model.enums.ReadStatus.READING, org.booklore.model.enums.ReadStatus.RE_READING)
               AND (b.deleted IS NULL OR b.deleted = false)
-              AND bf.isBookFormat = true
-              AND bf.bookType <> org.booklore.model.enums.BookFileType.AUDIOBOOK
+              AND EXISTS (
+                  SELECT 1 FROM b.bookFiles bf
+                  WHERE bf.isBookFormat = true
+                    AND bf.bookType <> org.booklore.model.enums.BookFileType.AUDIOBOOK
+              )
               AND b.library.id IN :libraryIds
               AND ubp.lastReadTime IS NOT NULL
             ORDER BY ubp.lastReadTime DESC
-            SELECT DISTINCT ubp.book.id FROM UserBookProgressEntity ubp
+            SELECT ubp.book.id FROM UserBookProgressEntity ubp
             JOIN ubp.book b
-            JOIN b.bookFiles bf
             WHERE ubp.user.id = :userId
               AND ubp.readStatus IN (org.booklore.model.enums.ReadStatus.READING, org.booklore.model.enums.ReadStatus.RE_READING)
               AND (b.deleted IS NULL OR b.deleted = false)
-              AND bf.isBookFormat = true
-              AND bf.bookType = org.booklore.model.enums.BookFileType.AUDIOBOOK
+              AND EXISTS (
+                  SELECT 1 FROM b.bookFiles bf
+                  WHERE bf.isBookFormat = true
+                    AND bf.bookType = org.booklore.model.enums.BookFileType.AUDIOBOOK
+              )
               AND b.library.id IN :libraryIds
               AND ubp.lastReadTime IS NOT NULL
             ORDER BY ubp.lastReadTime DESC
In current MariaDB and MySQL versions, is `SELECT DISTINCT a ... ORDER BY b` valid when `b` is not part of the select list?
🤖 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/repository/UserBookProgressRepository.java`
around lines 212 - 244, Both continue-reading queries (the `@Query` for
findTopContinueReadingBookIds and the similar audiobook query) currently join
b.bookFiles and use SELECT DISTINCT ubp.book.id which causes duplicate/ordering
issues; replace the JOIN with either (a) a GROUP BY ubp.book.id and ORDER BY
MAX(ubp.lastReadTime) DESC so duplicates from bookFiles are collapsed, or (b)
remove the JOIN entirely and use an EXISTS subquery against BookFile (e.g. WHERE
EXISTS (SELECT bf FROM BookFileEntity bf WHERE bf.book = b AND bf.isBookFormat =
true AND bf.bookType [= or <>]
org.booklore.model.enums.BookFileType.AUDIOBOOK)), keeping the same predicates
for readStatus, libraryIds and lastReadTime; apply the same rewrite to both the
findTopContinueReadingBookIds query and the audiobook variant so ORDER BY uses
the aggregated/max lastReadTime or is unaffected by multiple bookFiles.
booklore-api/src/main/java/org/booklore/app/service/AppBookService.java (1)

148-149: ⚠️ Potential issue | 🟠 Major

Guard library scope before invoking repository methods with IN :libraryIds.

Line 148 and Line 171 pass accessibleLibraryIds directly, but this value can be null (admin) or empty (no assigned libraries). That creates a runtime risk for the repository IN :libraryIds queries.

Service-side branching sketch
     int maxItems = validateLimit(limit, 10);
+    if (accessibleLibraryIds != null && accessibleLibraryIds.isEmpty()) {
+        return Collections.emptyList();
+    }
 
-    List<Long> topIds = userBookProgressRepository.findTopContinueReadingBookIds(
-            userId, accessibleLibraryIds, PageRequest.of(0, maxItems));
+    Pageable page = PageRequest.of(0, maxItems);
+    List<Long> topIds = (accessibleLibraryIds == null)
+            ? userBookProgressRepository.findTopContinueReadingBookIdsAllLibraries(userId, page)
+            : userBookProgressRepository.findTopContinueReadingBookIds(userId, accessibleLibraryIds, page);
     int maxItems = validateLimit(limit, 10);
+    if (accessibleLibraryIds != null && accessibleLibraryIds.isEmpty()) {
+        return Collections.emptyList();
+    }
 
-    List<Long> topIds = userBookProgressRepository.findTopContinueListeningBookIds(
-            userId, accessibleLibraryIds, PageRequest.of(0, maxItems));
+    Pageable page = PageRequest.of(0, maxItems);
+    List<Long> topIds = (accessibleLibraryIds == null)
+            ? userBookProgressRepository.findTopContinueListeningBookIdsAllLibraries(userId, page)
+            : userBookProgressRepository.findTopContinueListeningBookIds(userId, accessibleLibraryIds, page);
#!/bin/bash
set -euo pipefail

# Verify null/empty contract in service scope resolution:
rg -nP --type=java 'private Set<Long> getAccessibleLibraryIds|return null;|Collections\.emptySet\(\)' \
  booklore-api/src/main/java/org/booklore/app/service/AppBookService.java

# Verify repository callsites that pass the scope directly:
rg -nP --type=java 'findTopContinue(Read|Listen)ingBookIds\s*\(' \
  booklore-api/src/main/java/org/booklore/app/service/AppBookService.java

# Verify repository queries still require IN :libraryIds:
rg -nP --type=java 'b\.library\.id IN :libraryIds' \
  booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java

Also applies to: 171-172

🤖 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/app/service/AppBookService.java`
around lines 148 - 149, The call to
userBookProgressRepository.findTopContinueReadingBookIds (and the similar
findTopContinueListeningBookIds) passes accessibleLibraryIds directly which can
be null (admin) or empty (no libs) and will break the "IN :libraryIds" JPQL;
before invoking these repository methods check accessibleLibraryIds: if
accessibleLibraryIds is empty return Collections.emptyList() (or an empty
result) immediately, and if accessibleLibraryIds is null use the repository
variant that does not filter by library (or add a dedicated call like
findTopContinueReadingBookIdsForAdmin(userId, PageRequest)) ; update the code
paths around topIds (and the analogous listeningIds call) to branch accordingly
so you never invoke the IN :libraryIds query with a null/empty collection.
booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java (2)

102-106: ⚠️ Potential issue | 🟠 Major

Reject non-positive batchSize before entering the loop.

Line 105 never advances when batchSize == 0, and negative values make the subList bounds invalid. Guard it upfront so a bad config can't hang this job.

🛠️ Proposed fix
 `@Transactional`
 public void saveRecommendationsInBatches(Map<Long, Set<BookRecommendationLite>> recommendations, int batchSize) {
+    if (batchSize <= 0) {
+        throw new IllegalArgumentException("batchSize must be greater than 0");
+    }
     List<Long> bookIds = new ArrayList<>(recommendations.keySet());
     for (int i = 0; i < bookIds.size(); i += batchSize) {
🤖 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/book/BookQueryService.java`
around lines 102 - 106, In saveRecommendationsInBatches(Map<Long,
Set<BookRecommendationLite>> recommendations, int batchSize) validate batchSize
before the loop (e.g., if batchSize <= 0) and fail fast by throwing an
IllegalArgumentException with a clear message (or returning early if that
matches project conventions); this prevents an infinite loop when batchSize == 0
and invalid subList bounds for negative sizes — add the check at the top of the
method before creating bookIds and entering the for-loop.

47-53: ⚠️ Potential issue | 🟠 Major

totalElements is still based on the unfiltered page.

Lines 49-53 apply contentRestrictionService after the repository page is sliced, but Line 53 keeps page.getTotalElements(). If restrictions drop any books, clients will get short/empty pages with totals that still count hidden rows. This needs query-side restriction or a recomputed total.

🤖 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/book/BookQueryService.java`
around lines 47 - 53, getAllBooksByLibraryIdsPaged uses
bookRepository.findAllWithMetadataByLibraryIdsPage then filters the page content
via contentRestrictionService.applyRestrictions, but still constructs the
PageImpl with page.getTotalElements() (which counts unfiltered rows). Fix by
ensuring the total reflects the filtered result: either push the restriction
into the query (update findAllWithMetadataByLibraryIdsPage to apply the same
contentRestriction predicates and return a correct count) or, if immediate fix
is required in getAllBooksByLibraryIdsPaged, compute a correct total from the
restriction (e.g., run a restricted-count query or recompute totalElements from
the restricted set when you can safely do so) and pass that recomputed total
into the PageImpl instead of page.getTotalElements(); reference methods:
getAllBooksByLibraryIdsPaged,
bookRepository.findAllWithMetadataByLibraryIdsPage,
contentRestrictionService.applyRestrictions, and PageImpl constructor.
🧹 Nitpick comments (1)
booklore-api/build.gradle.kts (1)

143-143: Prefer a configurable test heap instead of a fixed 2g.

A hard-coded value can be brittle on smaller CI/local runners. Making it property-driven keeps this optimization while improving portability.

♻️ Suggested change
 tasks.named<Test>("test") {
     useJUnitPlatform()
-    maxHeapSize = "2g"
+    maxHeapSize = providers.gradleProperty("testMaxHeap")
+        .orElse("2g")
+        .get()
     jvmArgs("-XX:+EnableDynamicAgentLoading")
     finalizedBy(tasks.named("jacocoTestReport"))
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@booklore-api/build.gradle.kts` at line 143, Replace the hard-coded test JVM
heap ("maxHeapSize = \"2g\"") with a property-driven value so CI/local runners
can override it; read a Gradle/project property (e.g. testHeapSize or via system
property) with a sensible default like "2g" and assign that value to maxHeapSize
in the test task configuration (referencing the existing maxHeapSize setting) so
callers can set -PtestHeapSize or an env-driven property to adjust the heap
without editing the build file.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@booklore-api/src/main/java/org/booklore/config/security/service/LoginRateLimitService.java`:
- Around line 23-29: LoginRateLimitService is dead code (annotated `@Service` but
never used); either remove it or integrate it—if you want to remove it, delete
the LoginRateLimitService class and references (constructor AuditService field,
attemptCache) and any unused imports to reduce clutter; if you intend to
keep/use it, wire it into the existing authentication flow (replace or add
alongside AuthRateLimitService in AuthenticationService where rate limiting is
applied), and update the cache configuration in LoginRateLimitService (increase
maximumSize from 1000 to ~10000 or swap attemptCache for an external store like
Redis) and ensure AuditService is correctly invoked where attempts are recorded.

In `@booklore-api/src/main/java/org/booklore/controller/BookController.java`:
- Around line 79-83: The OpenAPI doc currently hides pagination params by
annotating the Pageable parameter with `@Parameter`(hidden = true) in the
getBooksPaged method; replace that with the springdoc-supported annotation
`@ParameterObject` on the Pageable parameter so page/size/sort are exposed in the
generated contract (update the import to
org.springdoc.core.annotations.ParameterObject if missing and remove the
`@Parameter`(hidden = true) usage). Ensure the signature of getBooksPaged(Pageable
pageable) keeps the same parameter name and type but uses `@ParameterObject`
Pageable pageable so documentation shows page, size and sort.

---

Duplicate comments:
In `@booklore-api/src/main/java/org/booklore/app/service/AppBookService.java`:
- Around line 148-149: The call to
userBookProgressRepository.findTopContinueReadingBookIds (and the similar
findTopContinueListeningBookIds) passes accessibleLibraryIds directly which can
be null (admin) or empty (no libs) and will break the "IN :libraryIds" JPQL;
before invoking these repository methods check accessibleLibraryIds: if
accessibleLibraryIds is empty return Collections.emptyList() (or an empty
result) immediately, and if accessibleLibraryIds is null use the repository
variant that does not filter by library (or add a dedicated call like
findTopContinueReadingBookIdsForAdmin(userId, PageRequest)) ; update the code
paths around topIds (and the analogous listeningIds call) to branch accordingly
so you never invoke the IN :libraryIds query with a null/empty collection.

In
`@booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java`:
- Around line 212-244: Both continue-reading queries (the `@Query` for
findTopContinueReadingBookIds and the similar audiobook query) currently join
b.bookFiles and use SELECT DISTINCT ubp.book.id which causes duplicate/ordering
issues; replace the JOIN with either (a) a GROUP BY ubp.book.id and ORDER BY
MAX(ubp.lastReadTime) DESC so duplicates from bookFiles are collapsed, or (b)
remove the JOIN entirely and use an EXISTS subquery against BookFile (e.g. WHERE
EXISTS (SELECT bf FROM BookFileEntity bf WHERE bf.book = b AND bf.isBookFormat =
true AND bf.bookType [= or <>]
org.booklore.model.enums.BookFileType.AUDIOBOOK)), keeping the same predicates
for readStatus, libraryIds and lastReadTime; apply the same rewrite to both the
findTopContinueReadingBookIds query and the audiobook variant so ORDER BY uses
the aggregated/max lastReadTime or is unaffected by multiple bookFiles.

In `@booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java`:
- Around line 102-106: In saveRecommendationsInBatches(Map<Long,
Set<BookRecommendationLite>> recommendations, int batchSize) validate batchSize
before the loop (e.g., if batchSize <= 0) and fail fast by throwing an
IllegalArgumentException with a clear message (or returning early if that
matches project conventions); this prevents an infinite loop when batchSize == 0
and invalid subList bounds for negative sizes — add the check at the top of the
method before creating bookIds and entering the for-loop.
- Around line 47-53: getAllBooksByLibraryIdsPaged uses
bookRepository.findAllWithMetadataByLibraryIdsPage then filters the page content
via contentRestrictionService.applyRestrictions, but still constructs the
PageImpl with page.getTotalElements() (which counts unfiltered rows). Fix by
ensuring the total reflects the filtered result: either push the restriction
into the query (update findAllWithMetadataByLibraryIdsPage to apply the same
contentRestriction predicates and return a correct count) or, if immediate fix
is required in getAllBooksByLibraryIdsPaged, compute a correct total from the
restriction (e.g., run a restricted-count query or recompute totalElements from
the restricted set when you can safely do so) and pass that recomputed total
into the PageImpl instead of page.getTotalElements(); reference methods:
getAllBooksByLibraryIdsPaged,
bookRepository.findAllWithMetadataByLibraryIdsPage,
contentRestrictionService.applyRestrictions, and PageImpl constructor.

---

Nitpick comments:
In `@booklore-api/build.gradle.kts`:
- Line 143: Replace the hard-coded test JVM heap ("maxHeapSize = \"2g\"") with a
property-driven value so CI/local runners can override it; read a Gradle/project
property (e.g. testHeapSize or via system property) with a sensible default like
"2g" and assign that value to maxHeapSize in the test task configuration
(referencing the existing maxHeapSize setting) so callers can set -PtestHeapSize
or an env-driven property to adjust the heap without editing the build file.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 8bd5c23d-9ebc-4c4e-9c8f-470e7cca307c

📥 Commits

Reviewing files that changed from the base of the PR and between b219c0f and fd48901.

📒 Files selected for processing (20)
  • Dockerfile
  • booklore-api/build.gradle.kts
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java
  • booklore-api/src/main/java/org/booklore/config/security/oidc/OidcStateService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/AuthRateLimitService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/LoginRateLimitService.java
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/mapper/v2/BookMapperV2.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java
  • booklore-api/src/main/java/org/booklore/service/IconService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/metadata/BookMetadataService.java
  • booklore-api/src/main/java/org/booklore/util/koreader/EpubCfiService.java
  • booklore-api/src/main/resources/application.yaml
  • booklore-api/src/test/java/org/booklore/service/book/BookCreatorServiceTest.java
  • dev.docker-compose.yml
  • frontend/src/app/features/book/service/book.service.ts
💤 Files with no reviewable changes (1)
  • booklore-api/src/main/java/org/booklore/service/metadata/BookMetadataService.java
✅ Files skipped from review due to trivial changes (3)
  • booklore-api/src/main/java/org/booklore/service/IconService.java
  • booklore-api/src/main/java/org/booklore/config/security/oidc/OidcStateService.java
  • Dockerfile
🚧 Files skipped from review as they are similar to previous changes (2)
  • booklore-api/src/test/java/org/booklore/service/book/BookCreatorServiceTest.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
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/config/security/service/LoginRateLimitService.java
  • booklore-api/src/main/java/org/booklore/config/security/service/AuthRateLimitService.java
  • booklore-api/src/main/java/org/booklore/mapper/v2/BookMapperV2.java
  • booklore-api/src/main/java/org/booklore/util/koreader/EpubCfiService.java
  • booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
  • booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java
frontend/src/**/*.{ts,tsx,html,scss}

📄 CodeRabbit inference engine (AGENTS.md)

Use 2-space indentation in TypeScript, HTML, and SCSS files

Files:

  • frontend/src/app/features/book/service/book.service.ts
frontend/src/app/**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

frontend/src/app/**/*.{ts,tsx}: Prefer inject() over constructor injection
Follow frontend/eslint.config.js: component selectors use app-*, directive selectors use app*, and any is disallowed

Files:

  • frontend/src/app/features/book/service/book.service.ts
booklore-api/src/**/*Entity.java

📄 CodeRabbit inference engine (AGENTS.md)

Keep JPA entities on the *Entity suffix

Files:

  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
🧠 Learnings (9)
📚 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 : Use MapStruct for entity/DTO mapping

Applied to files:

  • booklore-api/src/main/java/org/booklore/mapper/v2/BookMapperV2.java
  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
📚 Learning: 2026-03-31T06:22:25.311Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 113
File: booklore-api/build.gradle.kts:89-90
Timestamp: 2026-03-31T06:22:25.311Z
Learning: When reviewing build logic or CI checks that call the JitPack build status API, do not treat a response like {"status":"none"} as evidence that a dependency is unavailable or that a build has failed. JitPack typically builds packages on-demand the first time they’re requested (e.g., via Gradle); "none" means the package isn’t pre-built/cached yet. Only raise a build failure concern when the status explicitly indicates an error/failure (e.g., failed/cancelled), or when dependent resolution actually fails.

Applied to files:

  • booklore-api/build.gradle.kts
📚 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/**/*Entity.java : Keep JPA entities on the `*Entity` suffix

Applied to files:

  • booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.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/model/entity/BookLoreUserEntity.java
  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
📚 Learning: 2026-03-24T18:58:08.199Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:291-294
Timestamp: 2026-03-24T18:58:08.199Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a Hardcover edition has no page count, falling back to the book-level page count (book.pages) is the preferred behavior. Skipping the sync entirely in this case is considered worse than using a slightly approximate total, because the page count is only a proxy for converting progress % to page numbers for the API. The Hardcover API allows setting page-based progress even when the edition lacks a page count, and progress displays in at least some UI views (e.g., the book detail dropdown).

Applied to files:

  • booklore-api/src/main/java/org/booklore/app/service/AppBookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookQueryService.java
📚 Learning: 2026-03-25T21:02:57.527Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 195
File: booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java:15-19
Timestamp: 2026-03-25T21:02:57.527Z
Learning: In `booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java` and `booklore-api/src/main/java/org/booklore/config/logging/filter/RequestLoggingFilter.java`, logging all request headers and payloads at DEBUG level is intentional and accepted behavior in this project. The previous `LoggingFilter` also logged all headers. No header redaction or payload scrubbing is required — the DEBUG log level is considered sufficient access control. The new filter is not profile-gated (unlike the old `Profile("dev")` filter) but relies on `logger.isDebugEnabled()` for gating.

Applied to files:

  • booklore-api/src/main/resources/application.yaml
📚 Learning: 2026-03-24T18:46:47.249Z
Learnt from: pedronave
Repo: grimmory-tools/grimmory PR: 173
File: booklore-api/src/main/java/org/booklore/service/hardcover/HardcoverSyncService.java:188-199
Timestamp: 2026-03-24T18:46:47.249Z
Learning: In grimmory-tools/grimmory (HardcoverSyncService.java), when a `hardcoverBookId` is stored in book metadata but is malformed (non-numeric), the preferred behavior is to return null and skip the sync rather than falling back to ISBN-based lookup. The reasoning is that an explicitly set bookId represents deliberate user intent, and silently resolving via ISBN could sync progress to a different book than intended.

Applied to files:

  • booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java
📚 Learning: 2026-03-25T19:09:09.638Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 189
File: booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java:113-116
Timestamp: 2026-03-25T19:09:09.638Z
Learning: In `booklore-api/src/main/java/org/booklore/service/kobo/KoboLibrarySyncService.java`, when deciding whether to forward the `Authorization` header to the upstream Kobo server proxy on the `/v1/library/sync` endpoint, a simple null check (`request.getHeader(HttpHeaders.AUTHORIZATION) != null`) is the correct guard. The header is either absent or present — if present it may or may not be a valid token, but validity is determined by the upstream Kobo server, not by Grimmory. Blank/whitespace intermediate states do not occur in practice, and pre-validating the token value before forwarding is not the responsibility of this service.

Applied to files:

  • booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java
📚 Learning: 2026-03-26T03:22:24.500Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 209
File: booklore-api/src/main/java/org/booklore/util/FileUtils.java:32-38
Timestamp: 2026-03-26T03:22:24.500Z
Learning: In `grimmory-tools/grimmory`, `BookFileEntity.fileSubPath` (in `booklore-api/src/main/java/org/booklore/model/entity/BookFileEntity.java`) is declared with `Column(name = "file_sub_path", length = 512, nullable = false)`, meaning it is never null. Do not flag null-guard concerns for this field.

Applied to files:

  • booklore-api/src/main/java/org/booklore/repository/UserBookProgressRepository.java
🔇 Additional comments (8)
booklore-api/src/main/java/org/booklore/util/koreader/EpubCfiService.java (1)

24-25: Cache tuning update looks consistent and safe in this scope.

Reducing size/TTL here is a clear performance-memory tradeoff, and this change is internally consistent with the existing cache lifecycle and invalidation flow.

booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java (1)

98-110: Correct Hibernate-safe equals/hashCode implementation.

The implementation properly addresses proxy handling by using Hibernate.getClass() for both class comparison and hash code generation. The ID-based equality with null check ensures correct behavior for both transient and persistent entities.

booklore-api/build.gradle.kts (1)

136-137: Verify behavior after disabling Hibernate association management.

Turning off enableAssociationManagement removes enhancer-driven bidirectional synchronization. Please confirm entity add/remove paths now fully maintain both sides manually to avoid inconsistent persistence state.

frontend/src/app/features/book/service/book.service.ts (1)

104-105: Re-check cache invalidation with the new staleTime (already flagged previously).

Line 105 introduces a 30s freshness window. Please verify BookPatchService.updateBookShelves(...) invalidates/refetches BOOKS_QUERY_KEY on success; otherwise shelf mutations may remain stale until freshness expires.

#!/bin/bash
set -euo pipefail

PATCH_FILE="$(fd -i 'book-patch.service.ts' | head -n1)"
BOOK_FILE="$(fd -i 'book.service.ts' | rg 'frontend/src/app/features/book/service/book.service.ts' | head -n1)"

echo "Inspecting: ${PATCH_FILE}"
rg -n -C3 'class BookPatchService|updateBookShelves\s*\(|invalidateBooksQuery|invalidateQueries|setQueryData|BOOKS_QUERY_KEY' "${PATCH_FILE}"

echo
echo "Inspecting: ${BOOK_FILE}"
rg -n -C3 'getBooksQueryOptions|staleTime|updateBookShelves\s*\(' "${BOOK_FILE}"

Expected verification result: a clear invalidation/refetch path for BOOKS_QUERY_KEY in the shelf-update mutation flow.

booklore-api/src/main/java/org/booklore/config/security/service/AuthRateLimitService.java (1)

23-29: Cache size reduction weakens brute-force protection.

Reducing maximumSize from 10,000 to 1,000 while tracking both IP-based and username-based keys (2 keys per login attempt) allows distributed attackers to exhaust the cache and evict legitimate rate-limit counters, effectively bypassing brute-force protection.

booklore-api/src/main/java/org/booklore/config/security/oidc/BackchannelLogoutService.java (1)

32-35: Reduced replay-protection cache enables potential token replay attacks.

Reducing maximumSize to 1,000 while the /backchannel-logout endpoint (per OidcAuthController.java:125-134) lacks rate limiting allows attackers to flood the endpoint with bogus tokens, evict legitimate JTIs, and replay previously-processed logout tokens.

dev.docker-compose.yml (1)

20-20: JVM tuning flags look reasonable; Shenandoah GC is available in the Gradle image.

The flags mirror the production Dockerfile settings, which is good for dev/prod parity. A few notes:

  1. JAVA_TOOL_OPTIONS applies to all Java processes in the container, including the Gradle daemon and the Spring Boot app it spawns via bootRun. This is likely the intended behavior but worth noting.

  2. The gradle:9.3.1-jdk25-alpine image uses Eclipse Temurin JDK 25, which includes Shenandoah GC as a production feature (JEP 521). This differs from the production eclipse-temurin:25-jre-alpine base image (JDK vs. JRE), but both support Shenandoah.

booklore-api/src/main/java/org/booklore/app/service/AppBookService.java (1)

41-41: Good move: class-level read-only transaction default is clean and consistent.

This reduces annotation repetition while keeping explicit write transactions possible on mutating methods.

@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.

Actionable comments posted: 2

♻️ Duplicate comments (1)
booklore-api/src/main/java/org/booklore/controller/BookController.java (1)

82-83: ⚠️ Potential issue | 🟡 Minor

Expose pageable query params in OpenAPI docs.

Line 83 hides Pageable with @Parameter(hidden = true), so page, size, and sort may not appear in generated API docs for this endpoint. Use @ParameterObject instead.

Suggested change
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.responses.ApiResponse;
 import io.swagger.v3.oas.annotations.responses.ApiResponses;
+import org.springdoc.core.annotations.ParameterObject;
@@
     `@GetMapping`("/page")
     public ResponseEntity<Page<Book>> getBooksPaged(
-            `@Parameter`(hidden = true) Pageable pageable) {
+            `@ParameterObject` Pageable pageable) {
         return ResponseEntity.ok(bookService.getBookDTOsPaged(pageable));
     }
For springdoc-openapi 3.0.2, what is the recommended way to document Spring Data Pageable query parameters in OpenAPI: `@Parameter`(hidden = true) Pageable or `@ParameterObject` Pageable?
🤖 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/controller/BookController.java`
around lines 82 - 83, The getBooksPaged method currently hides the Pageable
parameter with `@Parameter`(hidden = true), which prevents page/size/sort from
appearing in OpenAPI; replace that annotation with `@ParameterObject` (from
org.springdoc.api.annotations.ParameterObject) on the Pageable parameter so
Springdoc will expose the page, size and sort query params in the generated docs
and keep the method signature public ResponseEntity<Page<Book>>
getBooksPaged(`@ParameterObject` Pageable pageable).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@booklore-api/src/main/resources/application.yaml`:
- Around line 79-80: Remove the deprecated and unused Hibernate property by
deleting the plan_parameter_metadata_max_size entry and its comment from the
configuration block; keep only the active plan_cache_max_size setting
(hibernate.query.plan_cache_max_size) since
hibernate.query.plan_parameter_metadata_max_size is deprecated in Hibernate 6+
and has no effect.
- Around line 36-37: The Tomcat thread pool properties
(server.tomcat.threads.max and server.tomcat.threads.min-spare) are ineffective
when virtual threads are enabled via spring.threads.virtual.enabled; remove the
entries "max: 10" and "min-spare: 2" from application.yaml (the
server.tomcat.threads block) so only the virtual thread configuration remains
and avoid confusion about concurrency limits tied to server.tomcat.threads.*

---

Duplicate comments:
In `@booklore-api/src/main/java/org/booklore/controller/BookController.java`:
- Around line 82-83: The getBooksPaged method currently hides the Pageable
parameter with `@Parameter`(hidden = true), which prevents page/size/sort from
appearing in OpenAPI; replace that annotation with `@ParameterObject` (from
org.springdoc.api.annotations.ParameterObject) on the Pageable parameter so
Springdoc will expose the page, size and sort query params in the generated docs
and keep the method signature public ResponseEntity<Page<Book>>
getBooksPaged(`@ParameterObject` Pageable pageable).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b215e92e-885f-4077-a793-34b2276f3c83

📥 Commits

Reviewing files that changed from the base of the PR and between fd48901 and 44b2d25.

📒 Files selected for processing (7)
  • Dockerfile
  • booklore-api/build.gradle.kts
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
  • booklore-api/src/main/resources/application.yaml
  • dev.docker-compose.yml
✅ Files skipped from review due to trivial changes (1)
  • booklore-api/build.gradle.kts
🚧 Files skipped from review as they are similar to previous changes (4)
  • dev.docker-compose.yml
  • booklore-api/src/main/java/org/booklore/service/book/BookUpdateService.java
  • Dockerfile
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test Suite / Frontend Tests
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (java-kotlin)
🧰 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/controller/BookController.java
🧠 Learnings (1)
📚 Learning: 2026-03-25T21:02:57.527Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 195
File: booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java:15-19
Timestamp: 2026-03-25T21:02:57.527Z
Learning: In `booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java` and `booklore-api/src/main/java/org/booklore/config/logging/filter/RequestLoggingFilter.java`, logging all request headers and payloads at DEBUG level is intentional and accepted behavior in this project. The previous `LoggingFilter` also logged all headers. No header redaction or payload scrubbing is required — the DEBUG log level is considered sufficient access control. The new filter is not profile-gated (unlike the old `Profile("dev")` filter) but relies on `logger.isDebugEnabled()` for gating.

Applied to files:

  • booklore-api/src/main/resources/application.yaml
🔇 Additional comments (4)
booklore-api/src/main/java/org/booklore/controller/BookController.java (1)

42-43: Imports for pagination types look good.

Page and Pageable are correctly added for the new paginated endpoint.

booklore-api/src/main/resources/application.yaml (3)

38-38: Reasonable header-size bump.

This fits the remote-auth and forwarded-header shape of the rest of the config.


62-63: Nice to see the transaction-boundary toggles land together.

Bundling the autocommit change, open-in-view: false, and the related Hibernate connection/slow-query settings makes the PR’s service-layer transaction changes much easier to reason about.

Also applies to: 70-70, 90-93


82-85: Nice guardrail for the new pageable reads.

Turning this failure on next to the more conservative batch size is a good fit for the new /page flow in this PR.

Comment thread booklore-api/src/main/resources/application.yaml
Comment thread booklore-api/src/main/resources/application.yaml

@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.

♻️ Duplicate comments (1)
booklore-api/src/main/java/org/booklore/controller/BookController.java (1)

79-85: ⚠️ Potential issue | 🟡 Minor

Expose page/size/sort in OpenAPI for the new paged endpoint.

Line 83 hides Pageable, which typically removes pagination query params from generated docs. Switching to @ParameterObject keeps runtime behavior and improves API contract visibility.

Suggested change
+import org.springdoc.core.annotations.ParameterObject;
...
-    public ResponseEntity<Page<Book>> getBooksPaged(
-            `@Parameter`(hidden = true) Pageable pageable) {
+    public ResponseEntity<Page<Book>> getBooksPaged(
+            `@ParameterObject` Pageable pageable) {
         return ResponseEntity.ok(bookService.getBookDTOsPaged(pageable));
     }
#!/bin/bash
# Verify springdoc dependency and current annotation usage
rg -n --iglob "*build.gradle*" --iglob "pom.xml" "springdoc|openapi"
rg -n -C2 "@ParameterObject|@Parameter\\(hidden = true\\)|getBooksPaged\\(" \
  booklore-api/src/main/java/org/booklore/controller/BookController.java
🤖 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/controller/BookController.java`
around lines 79 - 85, The Pageable parameter in getBooksPaged is hidden from
OpenAPI by `@Parameter`(hidden = true); replace that with `@ParameterObject` on the
Pageable parameter (and add the import
org.springdoc.core.annotations.ParameterObject) so page/size/sort query params
are exposed while keeping runtime behavior; update the parameter annotation in
the getBooksPaged method and ensure springdoc is on the classpath if not
already.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@booklore-api/src/main/java/org/booklore/controller/BookController.java`:
- Around line 79-85: The Pageable parameter in getBooksPaged is hidden from
OpenAPI by `@Parameter`(hidden = true); replace that with `@ParameterObject` on the
Pageable parameter (and add the import
org.springdoc.core.annotations.ParameterObject) so page/size/sort query params
are exposed while keeping runtime behavior; update the parameter annotation in
the getBooksPaged method and ensure springdoc is on the classpath if not
already.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f50f251-08f1-4b5d-8ef4-a2f67545f9ae

📥 Commits

Reviewing files that changed from the base of the PR and between de9e9ad and 06d0f59.

📒 Files selected for processing (4)
  • booklore-api/build.gradle.kts
  • booklore-api/src/main/java/org/booklore/controller/BookController.java
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
  • booklore-api/src/main/resources/application.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • booklore-api/build.gradle.kts
  • booklore-api/src/main/java/org/booklore/service/book/BookService.java
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test Suite / Backend Tests
  • GitHub Check: Test Suite / Frontend Tests
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (java-kotlin)
🧰 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/controller/BookController.java
🧠 Learnings (1)
📚 Learning: 2026-03-25T21:02:57.527Z
Learnt from: imnotjames
Repo: grimmory-tools/grimmory PR: 195
File: booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java:15-19
Timestamp: 2026-03-25T21:02:57.527Z
Learning: In `booklore-api/src/main/java/org/booklore/config/logging/RequestLoggingFilterConfig.java` and `booklore-api/src/main/java/org/booklore/config/logging/filter/RequestLoggingFilter.java`, logging all request headers and payloads at DEBUG level is intentional and accepted behavior in this project. The previous `LoggingFilter` also logged all headers. No header redaction or payload scrubbing is required — the DEBUG log level is considered sufficient access control. The new filter is not profile-gated (unlike the old `Profile("dev")` filter) but relies on `logger.isDebugEnabled()` for gating.

Applied to files:

  • booklore-api/src/main/resources/application.yaml
🔇 Additional comments (9)
booklore-api/src/main/java/org/booklore/controller/BookController.java (1)

42-43: Pagination imports look correct.

Page and Pageable imports are appropriate for the new endpoint.

booklore-api/src/main/resources/application.yaml (8)

35-37: Remove ineffective Tomcat thread pool settings.

With virtual threads enabled at line 100, these server.tomcat.threads.* properties have no effect—Spring Boot dispatches requests to virtual threads, bypassing Tomcat's platform thread pool entirely.


82-83: Remove deprecated plan_parameter_metadata_max_size setting.

The plan_parameter_metadata_max_size property is deprecated since Hibernate 6.0 and is ignored. Only plan_cache_max_size is active.


65-65: Auto-commit settings are correctly paired.

The hikari.auto-commit: false setting at line 65 is properly paired with provider_disables_autocommit: true at line 94. This is the recommended configuration to avoid unnecessary JDBC autocommit checks.

Also applies to: 93-94


85-85: Good addition of pagination safety guard.

This setting will cause Hibernate to fail fast when attempting to paginate over collection fetches (which would otherwise silently fetch all data and paginate in memory). This helps catch N+1 and memory issues early.


88-88: Batch size reduction is reasonable.

Reducing from 500 to 100 is appropriate for a self-hosted application with smaller datasets. This balances memory usage with bulk operation efficiency.


38-38: LGTM.

The max-http-request-header-size is the correct property name for Spring Boot 3.x (replacing the deprecated max-http-header-size), and 64KB is a reasonable limit.


96-96: No changes needed—property name is correct.

The log_slow_query property is the correct Hibernate 6.x setting for slow query logging. The 500ms threshold is reasonable for identifying slow queries, and the configuration properly uses an environment variable with a sensible default.


73-73: OSIV disabled: current code is safe from LazyInitializationException through proper transaction management.

The codebase mitigates lazy loading risks by consistently returning DTOs from service methods within @Transactional boundaries. Controllers never receive entity objects—mappers convert entities to DTOs while the transaction (and Hibernate session) remains open. This prevents the LazyInitializationException that would otherwise occur when lazy associations are accessed after the session closes.

Key observations:

  • ShelfEntity and BookEntity have properly configured lazy associations with @BatchSize annotations
  • All service methods (getShelves, getShelf, getShelfBooks, etc.) are marked @Transactional and map entities to DTOs before returning
  • Controllers return DTO types (Shelf, Book, Library, etc.), never raw entities
  • Lazy loads occur during the mapping step while the transaction is active

However, there is room for optimization: ShelfRepository includes an unused findByIdWithUser() method with @EntityGraph, suggesting someone recognized the need for explicit fetch strategies but didn't apply it consistently. Consider standardizing repository queries with @EntityGraph or JOIN FETCH where lazy associations are accessed by mappers (e.g., accessing user.id in ShelfMapper) to avoid unnecessary secondary queries, though this is an optimization opportunity rather than a correctness issue.

@balazs-szucs balazs-szucs merged commit 0353531 into grimmory-tools:develop Apr 3, 2026
14 checks passed
zachyale pushed a commit to zachyale/grimmory that referenced this pull request Apr 17, 2026
…tching (grimmory-tools#176)

* fix(hibernate): improve transaction management and optimize entity fetching

* chore(tests): clean-up comments

* chore: fix config issue in the kotlin dsl

* fix(hibernate): fix entity equality checks

* fix(hibernate): add transactional read-only support to BookService

* Update booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(hibernate): remove redundant annotations in BookLoreUserEntity

* fix(hibernate): import Hibernate

* fix(tests): fix merge conflict caused errors

* refactor(book): optimize book retrieval methods and reduce cache sizes

* refactor(metadata): remove lazy loading from description field in BookMetadataEntity

* fix(build): increase max heap size for test tasks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
zachyale pushed a commit to zachyale/grimmory that referenced this pull request Apr 17, 2026
…tching (grimmory-tools#176)

* fix(hibernate): improve transaction management and optimize entity fetching

* chore(tests): clean-up comments

* chore: fix config issue in the kotlin dsl

* fix(hibernate): fix entity equality checks

* fix(hibernate): add transactional read-only support to BookService

* Update booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(hibernate): remove redundant annotations in BookLoreUserEntity

* fix(hibernate): import Hibernate

* fix(tests): fix merge conflict caused errors

* refactor(book): optimize book retrieval methods and reduce cache sizes

* refactor(metadata): remove lazy loading from description field in BookMetadataEntity

* fix(build): increase max heap size for test tasks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
zachyale pushed a commit that referenced this pull request Apr 17, 2026
…tching (#176)

* fix(hibernate): improve transaction management and optimize entity fetching

* chore(tests): clean-up comments

* chore: fix config issue in the kotlin dsl

* fix(hibernate): fix entity equality checks

* fix(hibernate): add transactional read-only support to BookService

* Update booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(hibernate): remove redundant annotations in BookLoreUserEntity

* fix(hibernate): import Hibernate

* fix(tests): fix merge conflict caused errors

* refactor(book): optimize book retrieval methods and reduce cache sizes

* refactor(metadata): remove lazy loading from description field in BookMetadataEntity

* fix(build): increase max heap size for test tasks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
zachyale pushed a commit that referenced this pull request Apr 22, 2026
…tching (#176)

* fix(hibernate): improve transaction management and optimize entity fetching

* chore(tests): clean-up comments

* chore: fix config issue in the kotlin dsl

* fix(hibernate): fix entity equality checks

* fix(hibernate): add transactional read-only support to BookService

* Update booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(hibernate): remove redundant annotations in BookLoreUserEntity

* fix(hibernate): import Hibernate

* fix(tests): fix merge conflict caused errors

* refactor(book): optimize book retrieval methods and reduce cache sizes

* refactor(metadata): remove lazy loading from description field in BookMetadataEntity

* fix(build): increase max heap size for test tasks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
dsmouse pushed a commit to dsmouse/grimmory that referenced this pull request May 11, 2026
…tching (grimmory-tools#176)

* fix(hibernate): improve transaction management and optimize entity fetching

* chore(tests): clean-up comments

* chore: fix config issue in the kotlin dsl

* fix(hibernate): fix entity equality checks

* fix(hibernate): add transactional read-only support to BookService

* Update booklore-api/src/main/java/org/booklore/model/entity/BookLoreUserEntity.java

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(hibernate): remove redundant annotations in BookLoreUserEntity

* fix(hibernate): import Hibernate

* fix(tests): fix merge conflict caused errors

* refactor(book): optimize book retrieval methods and reduce cache sizes

* refactor(metadata): remove lazy loading from description field in BookMetadataEntity

* fix(build): increase max heap size for test tasks

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
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.

4 participants