-
Notifications
You must be signed in to change notification settings - Fork 0
Description
開発概要
目的
- バックエンド(Go)における「N+1問題」を特定し、パフォーマンスを最適化する
- シフト一覧取得処理で発生しているN+1問題を解消し、レスポンス速度を大幅に改善する
- 特に、モバイルアプリのシフト一覧表示に使用される
ShiftCard関連の取得処理を最適化する - GORMのJOINクエリを使用して、ループ内での個別クエリ発行を排除する
開発期間
- 開始日:
- 締切日:
考えられる開発内容
1. 問題箇所の特定と分析
1.1 シフト一覧取得メソッドのN+1問題
GetShifts メソッド
ファイル: api/lib/usecase/shift_usecase.go:77-184
- 問題箇所を確認:
rows.Next()ループ内で各シフトに対して7つのクエリを発行taskRep.Find()- タスク情報placeRep.Find()- 場所情報userRep.Find()- ユーザー情報yearRep.Find()- 年情報dateRep.Find()- 日付情報timeRep.Find()- 時間情報weatherRep.Find()- 天気情報
- クエリ数: N件のシフトがある場合、1 + N×7 クエリが発行されることを確認
GetShiftsByUser メソッド
ファイル: api/lib/usecase/shift_usecase.go:284-393
- 問題箇所を確認: 同様にループ内で7つのクエリを発行
- クエリ数: 1 + N×7 クエリが発行されることを確認
GetShiftsByUserAndDateAndWeather メソッド
ファイル: api/lib/usecase/shift_usecase.go:395-504
- 問題箇所を確認: 同様にループ内で7つのクエリを発行
- クエリ数: 1 + N×7 クエリが発行されることを確認
GetShiftByID メソッド
ファイル: api/lib/usecase/shift_usecase.go:186-282
- 問題箇所を確認: 1件取得だが、7つのクエリを発行(N+1ではないが最適化可能)
- クエリ数: 7 クエリが発行されることを確認
1.2 ShiftCard関連のメンバー取得メソッドのN+1問題
GetUsersByShift メソッド
ファイル: api/lib/usecase/shift_usecase.go:506-583
- 問題箇所を確認:
rows.Next()ループ内で各ユーザーを個別に取得 - クエリ数: 1 + N クエリ(Nはユーザー数)が発行されることを確認
getShiftMembersForTime メソッド
ファイル: api/lib/usecase/shift_usecase.go:1025-1073
- 問題箇所を確認:
GetUsersByShiftで取得した各ユーザーに対して、grade と bureau を個別に取得 - クエリ数: 1 + M×2 クエリ(Mはユーザー数、各ユーザーに対してgradeとbureauの2クエリ)が発行されることを確認
getBeforeMembers メソッド
ファイル: api/lib/usecase/shift_usecase.go:1075-1147
- 問題箇所を確認: 同様に各ユーザーに対して grade と bureau を個別取得
- クエリ数: 1 + M×2 クエリが発行されることを確認
getAfterMembers メソッド
ファイル: api/lib/usecase/shift_usecase.go:1149-1226
- 問題箇所を確認: 同様に各ユーザーに対して grade と bureau を個別取得
- クエリ数: 1 + M×2 クエリが発行されることを確認
2. 新しいエンティティ型の追加
2.1 ShiftJoinResult エンティティの作成
ファイル: api/lib/entity/shift_join_result.go(新規作成)
-
ShiftJoinResult構造体を定義- Shift基本情報(ShiftID, IsAttendance, CreatedAt, UpdatedAt)
- Task情報(TaskID, TaskName, TaskColor, TaskURL, TaskRemark, MaxMember, TaskBureauID)
- Place情報(PlaceID, PlaceName)
- User情報(UserID, UserName, UserMail, UserBureauID, UserGradeID)
- Year/Date/Time/Weather情報(YearID, YearValue, DateID, DateValue, TimeID, TimeValue, WeatherID, WeatherValue)
- GORMタグを適切に設定(
gorm:"column:...")
2.2 UserWithGradeAndBureau エンティティの作成
ファイル: api/lib/entity/user_with_grade_bureau.go(新規作成)
-
UserWithGradeAndBureau構造体を定義- UserID, UserName, UserMail
- GradeID, GradeName
- BureauID, BureauName, BureauColor
- GORMタグを適切に設定(
gorm:"column:...")
3. リポジトリ層の拡張
3.1 ShiftRepository インターフェースの拡張
ファイル: api/lib/internals/repository/shift_repository.go
-
ShiftRepositoryインターフェースに新しいメソッドを追加:GetShiftsWithJoins(context.Context) ([]entity.ShiftJoinResult, error)GetShiftsByUserWithJoins(context.Context, string) ([]entity.ShiftJoinResult, error)GetShiftsByUserAndDateAndWeatherWithJoins(context.Context, string, string, string) ([]entity.ShiftJoinResult, error)GetShiftByIDWithJoins(context.Context, string) (entity.ShiftJoinResult, error)GetUsersByShiftWithJoins(context.Context, string, string, string, string, string) ([]entity.UserWithGradeAndBureau, error)
3.2 ShiftRepository 実装の追加
ファイル: api/lib/internals/repository/shift_repository.go
-
GetShiftsWithJoinsメソッドを実装- GORMのJOINクエリを使用して、shifts, tasks, places, users, years, dates, times, weathers を一括取得
- 既存の
shift_card_repository.goの実装パターンを参考にする
-
GetShiftsByUserWithJoinsメソッドを実装- ユーザーIDでフィルタリングしたJOINクエリ
-
GetShiftsByUserAndDateAndWeatherWithJoinsメソッドを実装- ユーザーID、日付ID、天気IDでフィルタリングしたJOINクエリ
-
GetShiftByIDWithJoinsメソッドを実装- シフトIDでフィルタリングしたJOINクエリ
-
GetUsersByShiftWithJoinsメソッドを実装- シフト条件でフィルタリングし、users, grades, bureaus をJOINで一括取得
4. UseCase層の修正
4.1 GetShifts メソッドの最適化
ファイル: api/lib/usecase/shift_usecase.go
-
GetShiftsメソッドを修正- 新しい
GetShiftsWithJoinsリポジトリメソッドを使用 ShiftJoinResultをentity.Shiftに変換するロジックを追加- 既存の動作を維持することを確認
- 新しい
4.2 GetShiftsByUser メソッドの最適化
ファイル: api/lib/usecase/shift_usecase.go
-
GetShiftsByUserメソッドを修正- 新しい
GetShiftsByUserWithJoinsリポジトリメソッドを使用 ShiftJoinResultをentity.Shiftに変換するロジックを追加- 既存の動作を維持することを確認
- 新しい
4.3 GetShiftsByUserAndDateAndWeather メソッドの最適化
ファイル: api/lib/usecase/shift_usecase.go
-
GetShiftsByUserAndDateAndWeatherメソッドを修正- 新しい
GetShiftsByUserAndDateAndWeatherWithJoinsリポジトリメソッドを使用 ShiftJoinResultをentity.Shiftに変換するロジックを追加- 既存の動作を維持することを確認
- 新しい
4.4 GetShiftByID メソッドの最適化
ファイル: api/lib/usecase/shift_usecase.go
-
GetShiftByIDメソッドを修正- 新しい
GetShiftByIDWithJoinsリポジトリメソッドを使用 ShiftJoinResultをentity.Shiftに変換するロジックを追加- 既存の動作を維持することを確認
- 新しい
4.5 GetUsersByShift メソッドの最適化
ファイル: api/lib/usecase/shift_usecase.go
-
GetUsersByShiftメソッドを修正- 新しい
GetUsersByShiftWithJoinsリポジトリメソッドを使用 UserWithGradeAndBureauをentity.ShiftUsersに変換するロジックを追加- grade と bureau の情報も含めて取得することを確認
- 既存の動作を維持することを確認
- 新しい
4.6 getShiftMembersForTime メソッドの改善
ファイル: api/lib/usecase/shift_usecase.go
-
getShiftMembersForTimeメソッドを確認- 最適化された
GetUsersByShiftを使用することで、grade/bureau の個別取得が不要になることを確認 - 必要に応じてコードを簡素化
- 最適化された
4.7 getBeforeMembers メソッドの改善
ファイル: api/lib/usecase/shift_usecase.go
-
getBeforeMembersメソッドを確認- 最適化された
GetUsersByShiftを使用することで、grade/bureau の個別取得が不要になることを確認 - 必要に応じてコードを簡素化
- 最適化された
4.8 getAfterMembers メソッドの改善
ファイル: api/lib/usecase/shift_usecase.go
-
getAfterMembersメソッドを確認- 最適化された
GetUsersByShiftを使用することで、grade/bureau の個別取得が不要になることを確認 - 必要に応じてコードを簡素化
- 最適化された
5. 動作確認とテスト
5.1 単体テスト
- 新しいリポジトリメソッドの動作確認
- JOINクエリが正しく実行されることを確認
- 結果が正しくマッピングされることを確認
- UseCaseメソッドの動作確認
- 既存の動作が維持されることを確認
- エラーハンドリングが適切に実装されていることを確認
5.2 パフォーマンステスト
- クエリ数の確認
- 修正前: N件のシフト取得で 1 + N×7 クエリ
- 修正後: N件のシフト取得で 1 クエリになることを確認
- レスポンス時間の測定
- 100件のシフト取得で約700倍の改善(701クエリ → 1クエリ)を確認
- 10人のユーザー取得(grade/bureau含む)で約21倍の改善(21クエリ → 1クエリ)を確認
5.3 統合テスト
- APIエンドポイントの動作確認
- シフト一覧取得APIが正常に動作することを確認
- ShiftCard取得APIが正常に動作することを確認
- モバイルアプリからのリクエストが正常に処理されることを確認
備考
現在の問題コード例
GetShifts メソッドの問題箇所
for rows.Next() {
// ... shift基本情報の取得 ...
// ループ内で7つのクエリを発行(N+1問題)
row, err := a.taskRep.Find(c, TaskID)
row, err = a.placeRep.Find(c, PlaceID)
row, err = a.userRep.Find(c, UserID)
row, err = a.yearRep.Find(c, YearID)
row, err = a.dateRep.Find(c, DateID)
row, err = a.timeRep.Find(c, TimeID)
row, err = a.weatherRep.Find(c, WeatherID)
shifts = append(shifts, shift)
}修正後のコード案(JOINクエリを使用)
// リポジトリ層で1回のJOINクエリで一括取得
results, err := a.rep.GetShiftsWithJoins(c)
if err != nil {
return nil, err
}
// 結果をentity.Shiftに変換
var shifts []entity.Shift
for _, result := range results {
shift := convertShiftJoinResultToShift(result)
shifts = append(shifts, shift)
}期待される効果
クエリ数の変化
修正前
GetShifts: N件のシフト → 1 + N×7 クエリGetShiftsByUser: N件のシフト → 1 + N×7 クエリGetShiftsByUserAndDateAndWeather: N件のシフト → 1 + N×7 クエリGetUsersByShift: M人のユーザー → 1 + M クエリgetShiftMembersForTime: M人のユーザー → 1 + M×2 クエリ(grade + bureau)
修正後
GetShifts: N件のシフト → 1 クエリ(JOINで一括取得)GetShiftsByUser: N件のシフト → 1 クエリGetShiftsByUserAndDateAndWeather: N件のシフト → 1 クエリGetUsersByShift: M人のユーザー → 1 クエリ(JOINでgrade/bureauも含めて取得)getShiftMembersForTime: M人のユーザー → 1 クエリ(GetUsersByShiftの最適化により)
パフォーマンス改善
- 100件のシフト取得: 701クエリ → 1クエリ(約700倍の改善)
- 10人のユーザー取得(grade/bureau含む): 21クエリ → 1クエリ(約21倍の改善)
実装上の注意事項
- 既存の
shift_card_repository.goの実装パターンを参考にする - GORMのDBインスタンスは
db.Client.GormDB()で取得可能 - 既存のメソッドは後方互換性のため残す(段階的な移行を想定)
- エラーハンドリングを適切に実装する
- JOINクエリの結果を既存のエンティティ型に変換する際は、データの整合性を確認する
変更ファイル
新規作成
api/lib/entity/shift_join_result.go- JOINクエリ結果用エンティティ(Shift)api/lib/entity/user_with_grade_bureau.go- JOINクエリ結果用エンティティ(User + Grade + Bureau)
修正
api/lib/internals/repository/shift_repository.go- 新しいJOINクエリメソッドの追加api/lib/usecase/shift_usecase.go- 既存メソッドの最適化
参考実装
既存の最適化済み実装を参考にします:
api/lib/internals/repository/shift_card_repository.go-GetOptimizedShiftDataメソッド- GORMのJOINクエリを使用した実装例
Table(),Select(),Joins(),Where(),Scan()の使用方法
今後の拡張予定
- 他のリポジトリメソッドでも同様のN+1問題がないか確認
- パフォーマンスモニタリングの導入
- クエリログの分析ツールの導入
参考
開発の流れ
- PMにIssue(タスク)をもらう
- 開発をする(↓の「リンク」の『開発のやり方』を見よう!)
- チェックボックスを押していこう
- ヤバい状況になったらIssueの右側にあるStatusを「Help」にしてPMにSlackで連絡しよう
- チェックボックスが全部押せたらプルリクを作ろう
- レビューを待とう
- 修正点があれば修正しよう。なければPMがマージします!お疲れ様!
SeeFTのタスク管理のルール
- タスクは全てGit-Hub Projectで管理する
- 全てのタスクに期日を決める
- 毎週タスクの進捗を確認する(MTに出られない人はSlackで報告)
- 毎週忙しさ(消化できるタスク量)を共有する
- Helpは余裕のある人がいれば巻き取る。いなければ期日を変更する