MTM-6: bookmark persistence foundation (article_bookmarks)#181
Conversation
Mirror the favorite feature across the persistence layer: - V3 Flyway migration creating article_bookmarks (composite PK + created_at) - core/bookmark domain entity + repository interface - MyBatis mapper/XML + MyBatis-backed repository impl (idempotent save) - ArticleBookmarksReadService (isUserBookmark, userBookmarks, cursor query ordered by bookmark created_at desc) - persistence integration tests for repository and read service
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Per orchestrator decision (Option 2): order article_bookmarks.created_at DESC for NEXT and ASC for PREV (cursor predicate unchanged: NEXT < cursor, PREV > cursor), matching ArticleReadService/CommentReadService so MTM-3 can wire it with the standard Collections.reverse-on-PREV handling. Cursor stays keyed on bookmark created_at (D1 unchanged). Update PREV assertion and add test covering PREV paging when results exceed the page size (asserts the nearest, non-overlapping page).
There was a problem hiding this comment.
🚩 No API controller or application service wires up the new bookmark feature
This PR adds domain model, repository, read service, migration, and tests for bookmarks, but no REST controller, GraphQL mutation, or application-layer service consumes these components. The bookmark feature is not yet exposed to any client. This is presumably intentional (incremental delivery of the persistence layer first), but worth confirming that a follow-up PR is planned to wire up API endpoints.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Intentional. MTM-6 is the persistence-only foundation for the Reading List epic (MTM-1); REST controller and GraphQL mutation/query are explicitly out of scope here and are delivered in later stories (the query-service cursor wiring is MTM-3). No client-facing wiring in this PR by design.
Summary
Sequential foundation for the Reading List epic (MTM-1). Adds the bookmark persistence layer by mirroring the existing
favoritefeature so the pattern is immediately recognizable. Pure persistence — no REST/GraphQL/ArticleData/ArticleQueryServicechanges (those land in later stories on top of this).Design decision D1 (pre-decided): the reading list cursors on
article_bookmarks.created_at(bookmark time), not the article timestamp. D1 fixes the cursor source; the asc/desc-per-direction mechanics follow the existing repo pattern (see read-service note below). Query-service cursor wiring is MTM-3.What changed
MTM-7 —
V3__create_article_bookmarks.sql(additive; V1/V2 untouched):created_atis new vsarticle_favoritesto support "most-recent-bookmarked first" ordering. No FKs (consistent with current schema style).MTM-8 —
core/bookmark/:ArticleBookmark(articleId, userId)entity (same Lombok asArticleFavorite) +ArticleBookmarkRepository(save,find,remove).MTM-9 — persistence impl:
ArticleBookmarkMapper(@Mapper) +ArticleBookmarkMapper.xml;MyBatisArticleBookmarkRepository(@Repository) with idempotent save:Insert omits
created_atso the DB default applies.MTM-10 —
ArticleBookmarksReadService(@Mapper) + XML:isUserBookmark(userId, articleId)→count(1)mapped to booleanuserBookmarks(ids, currentUser)→ only the current user's ids (privacy), mirrorsuserFavoritesfindUserBookmarkedArticleIdsWithCursor(userId, page)→ cursor oncreated_at, direction-dependent ordering (see below),limit #{page.queryLimit}MTM-11 — integration tests (
src/test/java/io/spring/infrastructure/bookmark/, extendDbTestBase):MyBatisArticleBookmarkRepositoryTest: save→find present; save twice→exactly one row (idempotent, no exception); remove→empty; remove non-existent→no-op.ArticleBookmarksReadServiceTest:isUserBookmarktrue/false;userBookmarksexcludes other users' ids; cursor query ordering NEXT/PREV honoring limit; PREV when results exceed page size returns the nearest, non-overlapping page; empty list→empty. Rows inserted with explicit distinctcreated_atfor deterministic ordering.Cursor ordering (Devin Review follow-up, orchestrator-approved)
The first revision hardcoded
order by created_at descfor both directions. That returns the wrong page forPREVonce results above the cursor exceed the page size. Now it matchesArticleReadService/CommentReadService:NEXT: created_at < cursor order by created_at desc PREV: created_at > cursor order by created_at asc limit #{page.queryLimit}MTM-3 can then apply the same
trim extra → Collections.reverse() on PREVhandling used byArticleQueryService.findRecentArticlesWithCursorto always return a stable newest-bookmarked-first page. D1 (cursor keyed on bookmarkcreated_at) is unchanged.Test setup note
The test profile caps Flyway at V1 (
spring.flyway.target=1) to exclude V2 seed data, so V3's table isn't auto-created in@MybatisTestDBs. Bumping the target would load seed data and break existing read-model count assertions, so the two bookmark tests createarticle_bookmarksprogrammatically (same DDL as V3) in@BeforeEach. The real V3 migration is verified separately (see below).Verification gate (green)
Migration verified on a fresh DB via
bootRun:Note: bare
./gradlew clean testfails only onjacocoTestCoverageVerification(repo-wide 33%, pre-existing baseline unrelated to this change); CI excludes that task via-x jacocoTestCoverageVerification.Link to Devin session: https://partner-workshops.devinenterprise.com/sessions/922cc2316251497186672e6b2f01c386