開発概要
目的
バックエンド(Go)における「N+1問題」を特定し、パフォーマンスを最適化する
シフト一覧取得処理で発生しているN+1問題を解消し、レスポンス速度を大幅に改善する
特に、モバイルアプリのシフト一覧表示に使用される ShiftCard 関連の取得処理を最適化する
GORMのJOINクエリを使用して、ループ内での個別クエリ発行を排除する
開発期間
考えられる開発内容
1. 問題箇所の特定と分析
1.1 シフト一覧取得メソッドのN+1問題
GetShifts メソッド
ファイル : api/lib/usecase/shift_usecase.go:77-184
GetShiftsByUser メソッド
ファイル : api/lib/usecase/shift_usecase.go:284-393
GetShiftsByUserAndDateAndWeather メソッド
ファイル : api/lib/usecase/shift_usecase.go:395-504
GetShiftByID メソッド
ファイル : api/lib/usecase/shift_usecase.go:186-282
1.2 ShiftCard関連のメンバー取得メソッドのN+1問題
GetUsersByShift メソッド
ファイル : api/lib/usecase/shift_usecase.go:506-583
getShiftMembersForTime メソッド
ファイル : api/lib/usecase/shift_usecase.go:1025-1073
getBeforeMembers メソッド
ファイル : api/lib/usecase/shift_usecase.go:1075-1147
getAfterMembers メソッド
ファイル : api/lib/usecase/shift_usecase.go:1149-1226
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(新規作成)
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
4. UseCase層の修正
4.1 GetShifts メソッドの最適化
ファイル : api/lib/usecase/shift_usecase.go
4.2 GetShiftsByUser メソッドの最適化
ファイル : api/lib/usecase/shift_usecase.go
4.3 GetShiftsByUserAndDateAndWeather メソッドの最適化
ファイル : api/lib/usecase/shift_usecase.go
4.4 GetShiftByID メソッドの最適化
ファイル : api/lib/usecase/shift_usecase.go
4.5 GetUsersByShift メソッドの最適化
ファイル : api/lib/usecase/shift_usecase.go
4.6 getShiftMembersForTime メソッドの改善
ファイル : api/lib/usecase/shift_usecase.go
4.7 getBeforeMembers メソッドの改善
ファイル : api/lib/usecase/shift_usecase.go
4.8 getAfterMembers メソッドの改善
ファイル : api/lib/usecase/shift_usecase.go
5. 動作確認とテスト
5.1 単体テスト
5.2 パフォーマンステスト
5.3 統合テスト
備考
現在の問題コード例
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は余裕のある人がいれば巻き取る。いなければ期日を変更する
リンク
開発概要
目的
ShiftCard関連の取得処理を最適化する開発期間
考えられる開発内容
1. 問題箇所の特定と分析
1.1 シフト一覧取得メソッドのN+1問題
GetShiftsメソッドファイル:
api/lib/usecase/shift_usecase.go:77-184rows.Next()ループ内で各シフトに対して7つのクエリを発行taskRep.Find()- タスク情報placeRep.Find()- 場所情報userRep.Find()- ユーザー情報yearRep.Find()- 年情報dateRep.Find()- 日付情報timeRep.Find()- 時間情報weatherRep.Find()- 天気情報GetShiftsByUserメソッドファイル:
api/lib/usecase/shift_usecase.go:284-393GetShiftsByUserAndDateAndWeatherメソッドファイル:
api/lib/usecase/shift_usecase.go:395-504GetShiftByIDメソッドファイル:
api/lib/usecase/shift_usecase.go:186-2821.2 ShiftCard関連のメンバー取得メソッドのN+1問題
GetUsersByShiftメソッドファイル:
api/lib/usecase/shift_usecase.go:506-583rows.Next()ループ内で各ユーザーを個別に取得getShiftMembersForTimeメソッドファイル:
api/lib/usecase/shift_usecase.go:1025-1073GetUsersByShiftで取得した各ユーザーに対して、grade と bureau を個別に取得getBeforeMembersメソッドファイル:
api/lib/usecase/shift_usecase.go:1075-1147getAfterMembersメソッドファイル:
api/lib/usecase/shift_usecase.go:1149-12262. 新しいエンティティ型の追加
2.1 ShiftJoinResult エンティティの作成
ファイル:
api/lib/entity/shift_join_result.go(新規作成)ShiftJoinResult構造体を定義gorm:"column:...")2.2 UserWithGradeAndBureau エンティティの作成
ファイル:
api/lib/entity/user_with_grade_bureau.go(新規作成)UserWithGradeAndBureau構造体を定義gorm:"column:...")3. リポジトリ層の拡張
3.1 ShiftRepository インターフェースの拡張
ファイル:
api/lib/internals/repository/shift_repository.goShiftRepositoryインターフェースに新しいメソッドを追加: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.goGetShiftsWithJoinsメソッドを実装shift_card_repository.goの実装パターンを参考にするGetShiftsByUserWithJoinsメソッドを実装GetShiftsByUserAndDateAndWeatherWithJoinsメソッドを実装GetShiftByIDWithJoinsメソッドを実装GetUsersByShiftWithJoinsメソッドを実装4. UseCase層の修正
4.1 GetShifts メソッドの最適化
ファイル:
api/lib/usecase/shift_usecase.goGetShiftsメソッドを修正GetShiftsWithJoinsリポジトリメソッドを使用ShiftJoinResultをentity.Shiftに変換するロジックを追加4.2 GetShiftsByUser メソッドの最適化
ファイル:
api/lib/usecase/shift_usecase.goGetShiftsByUserメソッドを修正GetShiftsByUserWithJoinsリポジトリメソッドを使用ShiftJoinResultをentity.Shiftに変換するロジックを追加4.3 GetShiftsByUserAndDateAndWeather メソッドの最適化
ファイル:
api/lib/usecase/shift_usecase.goGetShiftsByUserAndDateAndWeatherメソッドを修正GetShiftsByUserAndDateAndWeatherWithJoinsリポジトリメソッドを使用ShiftJoinResultをentity.Shiftに変換するロジックを追加4.4 GetShiftByID メソッドの最適化
ファイル:
api/lib/usecase/shift_usecase.goGetShiftByIDメソッドを修正GetShiftByIDWithJoinsリポジトリメソッドを使用ShiftJoinResultをentity.Shiftに変換するロジックを追加4.5 GetUsersByShift メソッドの最適化
ファイル:
api/lib/usecase/shift_usecase.goGetUsersByShiftメソッドを修正GetUsersByShiftWithJoinsリポジトリメソッドを使用UserWithGradeAndBureauをentity.ShiftUsersに変換するロジックを追加4.6 getShiftMembersForTime メソッドの改善
ファイル:
api/lib/usecase/shift_usecase.gogetShiftMembersForTimeメソッドを確認GetUsersByShiftを使用することで、grade/bureau の個別取得が不要になることを確認4.7 getBeforeMembers メソッドの改善
ファイル:
api/lib/usecase/shift_usecase.gogetBeforeMembersメソッドを確認GetUsersByShiftを使用することで、grade/bureau の個別取得が不要になることを確認4.8 getAfterMembers メソッドの改善
ファイル:
api/lib/usecase/shift_usecase.gogetAfterMembersメソッドを確認GetUsersByShiftを使用することで、grade/bureau の個別取得が不要になることを確認5. 動作確認とテスト
5.1 単体テスト
5.2 パフォーマンステスト
5.3 統合テスト
備考
現在の問題コード例
GetShifts メソッドの問題箇所
修正後のコード案(JOINクエリを使用)
期待される効果
クエリ数の変化
修正前
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の最適化により)パフォーマンス改善
実装上の注意事項
shift_card_repository.goの実装パターンを参考にするdb.Client.GormDB()で取得可能変更ファイル
新規作成
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メソッドTable(),Select(),Joins(),Where(),Scan()の使用方法今後の拡張予定
参考
開発の流れ
SeeFTのタスク管理のルール
リンク