Skip to content

fix: shift-cardsのGetUsersByShift呼び出し回数を削減しN+1を解消 - #438

Open
taminororo wants to merge 2 commits into
developfrom
fix/kanba/437-shift-cards-n-plus-one
Open

fix: shift-cardsのGetUsersByShift呼び出し回数を削減しN+1を解消#438
taminororo wants to merge 2 commits into
developfrom
fix/kanba/437-shift-cards-n-plus-one

Conversation

@taminororo

@taminororo taminororo commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

対応Issue

resolve #437

概要

GET /shift-cards/users/:user_id/dates/:date_id/weathers/:weather_id(モバイル「マイシフト」画面が使用)のカード生成で、GetUsersByShiftが15分スロットごと・前後スロット分で繰り返し呼ばれていたのを、カード単位の1クエリバッチ取得に変更した。

  • ShiftRepositoryUsersByTimesを追加(Users()のJOIN形状+ANY($4)配列パラメータ、user_repository.goFindByNamesと同じ考え方)
  • usecaseにgetUsersByTimes/toShiftMembersを追加し、createShiftCardFromGroupを再構成。グループ内の全time_id(本体スロット+前後1枠)を先に集めて1回だけバッチ取得する
  • GetUsersByShift自体・ShowUsersByShiftエンドポイントは無変更(単一time_id版として引き続き使用されるため)
  • GetUsersByShiftが内部で発行していたyear/date/time/weatherの単票Find4本は、呼び出し元のいずれからも参照されていなかったため、新しいバッチ取得メソッドでは意図的に取得しない

背景(なぜ今このタイミングで対応するか)

2026-07-20の負荷試験(hey -z 1m -c 400 -q 0.5、目標約200 req/s)でこのエンドポイントが実測28.4 req/s・平均12.2秒・p95 18.8秒、HTTP 500が153件・タイムアウトが203件を記録し不合格となった。DBリーダーCPUが最大約90%まで上昇しており、45th開催(2026年9月上旬)を控えたデプロイ判断に直結する。

docs/development/test-roadmap.mdはフェーズ2(repository層ゴールデンテスト、実DB統合)完了後にこの種のリファクタへ着手することを推奨しているが、フェーズ2は未着手(フェーズ1の割り振りが2026-07-23に出たばかり)。本番負荷試験が既に不合格となっている状況を踏まえ、本issueに限りフェーズ2を待たずに着手した。安全網としてgo-sqlmockによるクエリ回数アサーション+既存CI(go-test.yml)で代替している。

なお本件は既存issue #247のセクション「1.2 ShiftCard関連のメンバー取得メソッドのN+1問題」と同じ関数群を扱うが、#247のその記述内容自体(GetUsersByShift内部の個別ユーザー取得、grade/bureauの個別取得)は既に別途解消済みだったため、今回の負荷試験で新たに見つかった「呼び出し回数」の問題として#437を切り出している(詳細はissue本文参照)。

画面スクリーンショット等

  • 該当なし(バックエンドのみ)

テスト項目

  • cd api && go test ./... -count=1 がグリーン
  • shift_usecase_shiftcards_sqlmock_test.go: 単一カード(6スロット)でユーザー取得クエリが1回にまとまること
  • 同テスト: 2タスク2カードでもカードごとに1回(合計2回)にまとまること(MatchExpectationsInOrder(false)でmapの反復順に依存しない形)
  • レスポンス内容(ShiftMembers/BeforeMembers/AfterMembers)のアサーション
  • ローカルのdocker-compose環境(実Postgres)でシフトデータを投入し、修正前(git stashでリバート)と修正後でレスポンスJSONが完全に一致することを確認済み(air のホットリロードで両方の状態を実機確認)

備考

Summary by CodeRabbit

  • 改善

    • シフトカード表示時のメンバー情報取得を効率化し、複数の時間枠をまとめて処理することで、表示速度と応答性を改善しました。
    • 前後の時間枠を含むメンバー情報が、より安定してカードへ反映されるようになりました。
  • テスト

    • 複数時間枠、空の時間枠、複数カードでの表示など、主要なケースを検証するテストを追加しました。

createShiftCardFromGroupが1カード内の15分スロットごと・前後スロット分でGetUsersByShiftを
繰り返し呼び出しており、呼び出し1回につきJOIN1本+year/date/time/weatherの単票Find4本の
計5クエリを発行していた。UsersByTimes(ANY($4)による複数time_idの一括取得)を追加し、
カード単位で1回のバッチ取得にまとめることで呼び出し回数を削減する。

year/date/time/weatherの単票FindはgetShiftMembersForTime等のいずれからも参照されていなかった
ため、新しいバッチ取得メソッドでは意図的に取得しない。go-sqlmockによるクエリ回数アサーション
テストを追加し、実DBでの修正前後のレスポンス一致も確認済み。
@taminororo taminororo self-assigned this Jul 23, 2026
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@taminororo, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: b103c1b8-c2d5-49e9-ae43-8e162568ab0c

📥 Commits

Reviewing files that changed from the base of the PR and between 237f9a0 and 86a5d81.

📒 Files selected for processing (2)
  • api/lib/usecase/shift_usecase.go
  • api/lib/usecase/shift_usecase_shiftcards_sqlmock_test.go
📝 Walkthrough

Walkthrough

Shiftカード生成時に本体・前後スロットのユーザーを一括取得し、time_id別にメンバーへ変換する処理へ変更しました。リポジトリのSQL、テスト用依存関係、sqlmockテストも追加しています。

Changes

Shiftカードのメンバー一括取得

Layer / File(s) Summary
複数time_id取得クエリ
api/lib/internals/repository/shift_repository.go
UsersByTimesを追加し、複数time_idをANY配列条件で取得します。空配列では結果を返しません。
Shiftカード生成の一括化
api/lib/usecase/shift_usecase.go
カード内の本体・前後スロットのtime_idをまとめて取得し、usersByTimeからShiftMemberBeforeMembersAfterMembersを生成します。
一括取得の検証基盤とテスト
api/go.mod, api/lib/usecase/shift_usecase_shiftcards_sqlmock_test.go
sqlmockとgormの共有DB基盤を追加し、クエリ回数、空入力、複数カード、属性変換を検証します。

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GetShiftCardsByUserAndDateAndWeather
  participant shiftRepository
  participant SQLDatabase
  Client->>GetShiftCardsByUserAndDateAndWeather: Request shift cards
  GetShiftCardsByUserAndDateAndWeather->>shiftRepository: UsersByTimes for card time_ids
  shiftRepository->>SQLDatabase: Execute JOIN with ANY(time_ids)
  SQLDatabase-->>shiftRepository: Return user rows
  shiftRepository-->>GetShiftCardsByUserAndDateAndWeather: Return users by time_id
  GetShiftCardsByUserAndDateAndWeather-->>Client: Return shift cards and members
Loading

Possibly related PRs

  • NUTFes/SeeFT#259: 同じShiftカードのメンバー取得処理で、UsersByTimesと一括取得を扱っています。
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 主要変更であるshift-cardsのGetUsersByShift呼び出し削減とN+1解消を端的に表しています。
Description check ✅ Passed 対応Issue、概要、テスト項目、備考が揃っており、テンプレート要件を概ね満たしています。
Linked Issues check ✅ Passed #437の要件であるバッチ取得化、ANY配列利用、time_id振り分け、sqlmock検証が実装とテストで満たされています。
Out of Scope Changes check ✅ Passed 依存追加とテスト追加は本件のN+1解消に直接関連しており、明確な対象外変更は見当たりません。
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/kanba/437-shift-cards-n-plus-one

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
api/lib/usecase/shift_usecase.go (2)

168-172: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

コメントがGoDoc形式になっている。

getUsersByTimes(168-171行目)とtoShiftMembers(212行目)のコメントは、いずれも関数名を主語にした説明文(「〜は…」)で始まっており、GoDocの慣習的パターンに該当します。コーディングガイドラインでは「コメントは日本語で記述し、GoDoc形式は使用しない」と定められているため、関数名を主語にしない自由記述に変更することを推奨します。

As per coding guidelines, "コメントは日本語で記述し、GoDoc 形式は使用しない。"

✏️ 修正例
-// getUsersByTimes は同一task/year/date/weatherで複数time_idにまたがるシフトメンバーを
-// 1クエリでまとめて取得し、time_idごとに振り分ける(shift-cardsのN+1解消)。
-// year/date/time/weatherの個別Findは呼び出し元(getShiftMembersForTime等)で
-// 使われていなかったため、意図的に取得しない。
+// 同一task/year/date/weatherで複数time_idにまたがるシフトメンバーを1クエリでまとめて取得し、
+// time_idごとに振り分ける(shift-cardsのN+1解消)。
+// year/date/time/weatherの個別Findは呼び出し元(getShiftMembersForTime等)で
+// 使われていなかったため、意図的に取得しない。
-// toShiftMembers はUser群をShiftMemberに変換する(grade/bureauはマップから引く)
+// User群をShiftMemberに変換する(grade/bureauはマップから引く)

Also applies to: 212-212

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/lib/usecase/shift_usecase.go` around lines 168 - 172,
関数名を主語にしたGoDoc形式になっているgetUsersByTimesとtoShiftMembersのコメントを、関数名で始めない日本語の自由記述へ変更してください。コメントの内容自体は維持し、指定された2つの関数コメントのみを修正してください。

Source: Coding guidelines


616-661: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

getNextTimeString(c, last.Time.ID)が同一カードで重複して呼ばれている。

616行目(EndTime算出)と652行目(hasNext判定、今回新規追加)はいずれもa.getNextTimeString(c, last.Time.ID)を呼び出しており、返す値は同一です。さらに、最終スロットのeTime再計算(682行目、既存)でもshift.Time.ID == last.Time.IDの場合に同じ呼び出しが発生し、カード1件あたり最大3回の重複クエリが生じます(テストファイルのexpectNoMoreTimes(mock, 3)のコメントでもtime_id=7への3回のFindとして明記されています)。

本PRの目的がshift-cardsエンドポイントのDBラウンドトリップ削減であることを踏まえると、この結果を1回計算して再利用することで、カードごとに2回分のクエリをさらに削減できます。

♻️ 修正例(概要)
-	endTime, err := a.getNextTimeString(c, last.Time.ID)
+	nextOfLast, nextOfLastErr := a.getNextTimeString(c, last.Time.ID)
+	endTime := nextOfLast
+	err := nextOfLastErr
 	if err != nil {
 		endTime = last.Time.Time // フォールバック
 	}
...
-	if s, err := a.getNextTimeString(c, last.Time.ID); err == nil && s != "" {
-		nextStart = s
+	if nextOfLastErr == nil && nextOfLast != "" {
+		nextStart = nextOfLast

(末尾スロットのeTime再計算(682行目付近)にも同様にnextOfLastを再利用できます)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/lib/usecase/shift_usecase.go` around lines 616 - 661, Cache the result of
getNextTimeString(c, last.Time.ID) in the shift-card construction flow and reuse
it for ShiftCard.EndTime and the hasNext calculation instead of calling it
again. Also reuse the cached result in the final-slot eTime recalculation near
the existing shift.Time.ID == last.Time.ID handling, preserving the current
fallback and next-slot behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@api/lib/usecase/shift_usecase.go`:
- Around line 168-172:
関数名を主語にしたGoDoc形式になっているgetUsersByTimesとtoShiftMembersのコメントを、関数名で始めない日本語の自由記述へ変更してください。コメントの内容自体は維持し、指定された2つの関数コメントのみを修正してください。
- Around line 616-661: Cache the result of getNextTimeString(c, last.Time.ID) in
the shift-card construction flow and reuse it for ShiftCard.EndTime and the
hasNext calculation instead of calling it again. Also reuse the cached result in
the final-slot eTime recalculation near the existing shift.Time.ID ==
last.Time.ID handling, preserving the current fallback and next-slot behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: df7ce829-dd45-4904-9b80-c34699828f2b

📥 Commits

Reviewing files that changed from the base of the PR and between 19ffa76 and 237f9a0.

⛔ Files ignored due to path filters (1)
  • api/go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • api/go.mod
  • api/lib/internals/repository/shift_repository.go
  • api/lib/usecase/shift_usecase.go
  • api/lib/usecase/shift_usecase_shiftcards_sqlmock_test.go

getNextTimeString(last.Time.ID)がEndTime算出・前後判定・最終スロットのeTime
再計算で3回重複して呼ばれていたのを1回にまとめ、カードごとにさらにクエリを
削減した。コメントもGoDoc形式(関数名を主語にした文)からプロジェクト規約に
沿った形に修正。
taminororo added a commit that referenced this pull request Jul 29, 2026
インフラ班によるシナリオ①再試験の結果を8章として追記。シフトデータの
分散(1日50件→date_id1〜4に12件ずつ)、pgbouncer導入、N+1修正(PR #438)
それぞれの効果を切り分けて記録。pgbouncerは500エラー・タイムアウトを
解消するが、N+1修正はその後も独立して+31%スループット・約4倍のレイ
テンシ改善をもたらすことを実測。

また、本試験が2.4節の方針(隔離DBのみで試験)から外れ共有HAクラスタ上で
実施された点を確認事項として明記した。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

shift-cards メンバー取得のN+1問題解消(GetUsersByShift呼び出し回数削減)

1 participant