fix(api): batch-fetch comicMetadata to eliminate N+1 #2

Merged
zombor merged 1 commit from bd-grimmory-n55.4 into develop 2026-05-11 00:53:27 +00:00
Owner

Summary

Bead: grimmory-n55.4 (part of epic grimmory-n55 — Scale to tens of thousands of books)

BookMetadataEntity.comicMetadata was a LAZY @OneToOne with no @BatchSize, while every sibling association on the same entity had @BatchSize(20). Iterating any large book list (e.g. the unpaginated GET /api/v1/books) triggered an N+1 on comic_metadata even after the other associations were batched.

This change adds @BatchSize(20) on comicMetadata so it follows the same pattern as the rest of the entity.

What changed

  • BookMetadataEntity.java: one annotation.
  • BookMetadataComicBatchFetchTest.java: new @DataJpaTest using Hibernate Statistics that asserts the SQL statement count when accessing comicMetadata across N books is ceil(N/20) instead of N.

Baseline (production, 65,758 books)

GET /api/v1/books?withDescription=false&stripForListView=true

  • TTFB: 19.5s, payload 50.3 MB, pod heap +972 MiB.
  • This PR is one component of reducing that cost. Re-running tools/perf/measure-books-api.sh (PR #2, bd-grimmory-n55.1) after this merges should show measurable improvement.

Test plan

  • CI runs just api test and just api check. Agent had no Java 25 toolchain locally so these were not executed before push.
  • Manually re-run tools/perf/measure-books-api.sh against the production server after merge and capture the new TTFB on bead grimmory-n55.1.
## Summary Bead: **grimmory-n55.4** (part of epic **grimmory-n55** — Scale to tens of thousands of books) `BookMetadataEntity.comicMetadata` was a LAZY `@OneToOne` with no `@BatchSize`, while every sibling association on the same entity had `@BatchSize(20)`. Iterating any large book list (e.g. the unpaginated `GET /api/v1/books`) triggered an N+1 on `comic_metadata` even after the other associations were batched. This change adds `@BatchSize(20)` on `comicMetadata` so it follows the same pattern as the rest of the entity. ## What changed - `BookMetadataEntity.java`: one annotation. - `BookMetadataComicBatchFetchTest.java`: new `@DataJpaTest` using Hibernate `Statistics` that asserts the SQL statement count when accessing `comicMetadata` across N books is `ceil(N/20)` instead of `N`. ## Baseline (production, 65,758 books) `GET /api/v1/books?withDescription=false&stripForListView=true` - TTFB: 19.5s, payload 50.3 MB, pod heap +972 MiB. - This PR is one component of reducing that cost. Re-running `tools/perf/measure-books-api.sh` (PR #2, bd-grimmory-n55.1) after this merges should show measurable improvement. ## Test plan - [ ] CI runs `just api test` and `just api check`. Agent had no Java 25 toolchain locally so these were not executed before push. - [ ] Manually re-run `tools/perf/measure-books-api.sh` against the production server after merge and capture the new TTFB on bead grimmory-n55.1.
fix(api): batch-fetch comicMetadata to avoid N+1 on book iteration
Some checks failed
notify-discord-release-notes.yml / fix(api): batch-fetch comicMetadata to avoid N+1 on book iteration (push) Failing after 0s
notify-discord-release-notes.yml / fix(api): batch-fetch comicMetadata to avoid N+1 on book iteration (pull_request) Failing after 0s
CI - Validate / Upload Event File (pull_request) Failing after 2s
CodeQL / Analyze (actions) (pull_request) Failing after 2s
CodeQL / Analyze (javascript-typescript) (pull_request) Failing after 4s
CodeQL / Analyze (java-kotlin) (pull_request) Failing after 5s
CI - Validate / Check for DB Migrations (pull_request) Successful in 12s
CI - Semantic PR Title / Validate PR Title (pull_request) Successful in 2s
CI - Validate / Flyway DB Migration Preview (pull_request) Has been skipped
CI - Validate / Flyway Migration Check (pull_request) Successful in 0s
CI - Frontend Quality Thresholds / Frontend Lint Threshold Check (pull_request) Failing after 13m23s
CI - Validate / Backend Tests (pull_request) Failing after 2s
CI - Validate / Frontend Tests (pull_request) Failing after 3s
CI - Validate / Test Suite (pull_request) Failing after 0s
CI - Validate / Packaging Smoke Test (pull_request) Has been skipped
notify-discord-release-notes.yml / Merge pull request 'fix(api): batch-fetch comicMetadata to eliminate N+1' (#2) from bd-grimmory-n55.4 into develop (pull_request) Failing after 0s
5036fbd1f4
BookMetadataEntity.comicMetadata was a LAZY @OneToOne with no @BatchSize,
making it the outlier among the entity's associations (all siblings already
use @BatchSize(20)). Any mapper or service path that loaded books without
explicitly graphing comicMetadata then iterated and touched it triggered N
separate selects.

Adds @BatchSize(size = 20) on the comicMetadata field (option a in
grimmory-n55.4) to mirror the siblings. Smallest, lowest-risk change.

Adds BookMetadataComicBatchFetchTest, a @SpringBootTest-backed JPA test
that seeds 25 books with comic metadata, loads BookMetadataEntity via JPQL
without an EntityGraph, and asserts via Hibernate Statistics that touching
comicMetadata produces at most ceil(25/20) = 2 lazy selects rather than 25.

Refs: grimmory-n55.4
zombor merged commit 188aeb2e0d into develop 2026-05-11 00:53:27 +00:00
zombor deleted branch bd-grimmory-n55.4 2026-05-11 00:53:28 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
zombor/grimmory!2
No description provided.