|
| 1 | +# contest_task_pairs データ投入処理の実装 |
| 2 | + |
| 3 | +## 概要 |
| 4 | + |
| 5 | +`prisma/seed.ts` に `prisma/contest_task_pairs.ts` のデータ投入処理を追加します。 |
| 6 | + |
| 7 | +`addTasks()` と `addTask()` の実装パターンを参考に、`addContestTaskPairs()` と `addContestTaskPair()` を実装しています。 |
| 8 | + |
| 9 | +## 追加内容 |
| 10 | + |
| 11 | +### 1. インポート追加 |
| 12 | + |
| 13 | +`.fabbrica` から `defineContestTaskPairFactory` をインポート: |
| 14 | + |
| 15 | +```typescript |
| 16 | +import { |
| 17 | + initialize, |
| 18 | + defineContestTaskPairFactory, |
| 19 | + defineUserFactory, |
| 20 | + defineKeyFactory, |
| 21 | + defineTaskFactory, |
| 22 | + defineTagFactory, |
| 23 | + defineTaskTagFactory, |
| 24 | + defineTaskAnswerFactory, |
| 25 | + defineSubmissionStatusFactory, |
| 26 | + defineWorkBookFactory, |
| 27 | +} from './.fabbrica'; |
| 28 | +``` |
| 29 | + |
| 30 | +`contest_task_pairs` データをインポート: |
| 31 | + |
| 32 | +```typescript |
| 33 | +import { contest_task_pairs } from './contest_task_pairs'; |
| 34 | +``` |
| 35 | + |
| 36 | +### 2. 並行処理設定追加 |
| 37 | + |
| 38 | +`QUEUE_CONCURRENCY` に `contestTaskPairs` を追加: |
| 39 | + |
| 40 | +```typescript |
| 41 | +const QUEUE_CONCURRENCY = { |
| 42 | + users: Number(process.env.SEED_USERS_CONCURRENCY) || 2, |
| 43 | + tasks: Number(process.env.SEED_TASKS_CONCURRENCY) || 3, |
| 44 | + contestTaskPairs: Number(process.env.SEED_CONTEST_TASK_PAIRS_CONCURRENCY) || 2, |
| 45 | + tags: Number(process.env.SEED_TAGS_CONCURRENCY) || 2, |
| 46 | + taskTags: Number(process.env.SEED_TASK_TAGS_CONCURRENCY) || 2, |
| 47 | + submissionStatuses: Number(process.env.SEED_SUBMISSION_STATUSES_CONCURRENCY) || 2, |
| 48 | + answers: Number(process.env.SEED_ANSWERS_CONCURRENCY) || 2, |
| 49 | +} as const; |
| 50 | +``` |
| 51 | + |
| 52 | +### 3. main 関数に処理追加 |
| 53 | + |
| 54 | +```typescript |
| 55 | +async function main() { |
| 56 | + try { |
| 57 | + console.log('Seeding has been started.'); |
| 58 | + |
| 59 | + await addUsers(); |
| 60 | + await addTasks(); |
| 61 | + await addContestTaskPairs(); |
| 62 | + await addWorkBooks(); |
| 63 | + await addTags(); |
| 64 | + await addTaskTags(); |
| 65 | + await addSubmissionStatuses(); |
| 66 | + await addAnswers(); |
| 67 | + |
| 68 | + console.log('Seeding has been completed.'); |
| 69 | + } catch (e) { |
| 70 | + console.error('Failed to seed:', e); |
| 71 | + throw e; |
| 72 | + } |
| 73 | +} |
| 74 | +``` |
| 75 | + |
| 76 | +### 4. 投入処理関数追加 |
| 77 | + |
| 78 | +#### `addContestTaskPairs()` 関数 |
| 79 | + |
| 80 | +```typescript |
| 81 | +async function addContestTaskPairs() { |
| 82 | + console.log('Start adding contest task pairs...'); |
| 83 | + |
| 84 | + const contestTaskPairFactory = defineContestTaskPairFactory(); |
| 85 | + |
| 86 | + // Create a queue with limited concurrency for contest task pair operations |
| 87 | + const contestTaskPairQueue = new PQueue({ concurrency: QUEUE_CONCURRENCY.contestTaskPairs }); |
| 88 | + |
| 89 | + for (const pair of contest_task_pairs) { |
| 90 | + contestTaskPairQueue.add(async () => { |
| 91 | + try { |
| 92 | + const [registeredPair, registeredTask] = await Promise.all([ |
| 93 | + prisma.contestTaskPair.findUnique({ |
| 94 | + where: { |
| 95 | + contestId_taskId: { |
| 96 | + contestId: pair.contest_id, |
| 97 | + taskId: pair.problem_id, |
| 98 | + }, |
| 99 | + }, |
| 100 | + }), |
| 101 | + prisma.task.findUnique({ |
| 102 | + where: { task_id: pair.problem_id }, |
| 103 | + }), |
| 104 | + ]); |
| 105 | + |
| 106 | + if (!registeredTask) { |
| 107 | + console.warn( |
| 108 | + 'Skipped contest task pair due to missing task:', |
| 109 | + pair.problem_id, |
| 110 | + 'for contest', |
| 111 | + pair.contest_id, |
| 112 | + 'index', |
| 113 | + pair.problem_index, |
| 114 | + ); |
| 115 | + } else if (!registeredPair) { |
| 116 | + await addContestTaskPair(pair, contestTaskPairFactory); |
| 117 | + console.log( |
| 118 | + 'contest_id:', |
| 119 | + pair.contest_id, |
| 120 | + 'problem_index:', |
| 121 | + pair.problem_index, |
| 122 | + 'task_id:', |
| 123 | + pair.task_id, |
| 124 | + 'was registered.', |
| 125 | + ); |
| 126 | + } |
| 127 | + } catch (e) { |
| 128 | + console.error('Failed to add contest task pair', pair, e); |
| 129 | + } |
| 130 | + }); |
| 131 | + } |
| 132 | + |
| 133 | + await contestTaskPairQueue.onIdle(); // Wait for all contest task pairs to complete |
| 134 | + console.log('Finished adding contest task pairs.'); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +#### `addContestTaskPair()` 関数 |
| 139 | + |
| 140 | +```typescript |
| 141 | +async function addContestTaskPair( |
| 142 | + pairs: (typeof contest_task_pairs)[number], |
| 143 | + contestTaskPairFactory: ReturnType<typeof defineContestTaskPairFactory>, |
| 144 | +) { |
| 145 | + await contestTaskPairFactory.create({ |
| 146 | + contestId: pairs.contest_id, |
| 147 | + taskTableIndex: pairs.problem_index, |
| 148 | + taskId: pairs.task_id, |
| 149 | + }); |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +## 実装パターン |
| 154 | + |
| 155 | +`addTasks()` / `addTask()` と同じパターンを採用: |
| 156 | + |
| 157 | +- **重複チェック**:`findUnique()` で既存データをチェック |
| 158 | +- **並行処理**:`PQueue` を使用した並行処理制御 |
| 159 | +- **エラーハンドリング**:try-catch で例外処理 |
| 160 | +- **ログ出力**:処理開始・完了・エラーをログ出力 |
| 161 | + |
| 162 | +## contest_task_pairs データ構造 |
| 163 | + |
| 164 | +```typescript |
| 165 | +{ |
| 166 | + contest_id: string; // コンテストID(例:'tessoku-book') |
| 167 | + task_id: string; // タスクID(例:'typical90_s') |
| 168 | + problem_index: string; // 問題インデックス(例:'C18') |
| 169 | +} |
| 170 | +``` |
| 171 | + |
| 172 | +## 実行方法 |
| 173 | + |
| 174 | +```bash |
| 175 | +pnpm db:seed |
| 176 | +``` |
| 177 | + |
| 178 | +通常のシード実行で `addContestTaskPairs()` が呼び出されます。 |
| 179 | + |
| 180 | +## 環境変数による並行数調整 |
| 181 | + |
| 182 | +```bash |
| 183 | +SEED_CONTEST_TASK_PAIRS_CONCURRENCY=4 pnpm db:seed |
| 184 | +``` |
| 185 | + |
| 186 | +## 実装完了 |
| 187 | + |
| 188 | +2025-10-22 に実装完了。合計 13 個の `ContestTaskPair` レコードが正常に投入されました。 |
| 189 | + |
| 190 | +## 教訓と抽象化 |
| 191 | + |
| 192 | +### 1. ファクトリ再生成の必要性 |
| 193 | + |
| 194 | +**問題**: Prisma スキーマに新しいモデルを追加しても、`.fabbrica` に自動生成されない場合がある。 |
| 195 | + |
| 196 | +**原因**: スキーマ変更後に `prisma generate` を実行する必要があります。 |
| 197 | + |
| 198 | +**解決策**: |
| 199 | + |
| 200 | +```bash |
| 201 | +pnpm prisma generate |
| 202 | +``` |
| 203 | + |
| 204 | +このコマンドにより、新しいモデル用のファクトリが生成されます。 |
| 205 | + |
| 206 | +### 2. 既存パターンの活用による効率化 |
| 207 | + |
| 208 | +**パターン**: データ投入処理の実装パターンは統一する。 |
| 209 | + |
| 210 | +**利点**: |
| 211 | + |
| 212 | +- コードの一貫性が保たれる |
| 213 | +- デバッグやメンテナンスが容易 |
| 214 | +- 新しい開発者の理解が速い |
| 215 | + |
| 216 | +**実装パターン** (`addTasks()` と同じ): |
| 217 | + |
| 218 | +1. ファクトリをインスタンス化 |
| 219 | +2. `PQueue` で並行処理制御 |
| 220 | +3. `findUnique()` で重複チェック |
| 221 | +4. キューが空になるまで待機 |
| 222 | +5. 処理結果をログ出力 |
| 223 | + |
| 224 | +### 3. データ構造の名前の統一性 |
| 225 | + |
| 226 | +**注意点**: `contest_task_pairs.ts` ファイルのフィールド名が `problem_id` ですが、Prisma スキーマでは `taskId` です。 |
| 227 | + |
| 228 | +**推奨**: データファイルとスキーマのフィールド名を統一する、または明確なマッピングを文書化する。 |
| 229 | + |
| 230 | +**現在の対応**: |
| 231 | + |
| 232 | +```typescript |
| 233 | +// contest_task_pairs.ts から読み込まれるデータ |
| 234 | +{ |
| 235 | + contest_id: 'tessoku-book', |
| 236 | + problem_id: 'typical90_s', // ← 注意:problem_id |
| 237 | + problem_index: 'C18' |
| 238 | +} |
| 239 | + |
| 240 | +// Prisma への投入時にマッピング |
| 241 | +contestId: pair.contest_id, |
| 242 | +taskId: pair.problem_id, // ← problem_id を taskId に |
| 243 | +taskTableIndex: pair.problem_index |
| 244 | +``` |
| 245 | + |
| 246 | +### 4. 処理順序の設計 |
| 247 | + |
| 248 | +**重要**: `addContestTaskPairs()` は `addTasks()` の後に実行する。 |
| 249 | + |
| 250 | +**理由**: `ContestTaskPair` は `taskId` を参照します。外部キー制約により、参照先が存在する必要があります。 |
| 251 | + |
| 252 | +**処理順序**: |
| 253 | + |
| 254 | +1. `addUsers()` - ユーザー作成 |
| 255 | +2. `addTasks()` - タスク作成 ⭐ 先 |
| 256 | +3. `addContestTaskPairs()` - コンテスト-タスク ペア ⭐ 後 |
| 257 | +4. `addWorkBooks()` - ワークブック作成 |
| 258 | + |
| 259 | +### 5. 環境変数による動的調整 |
| 260 | + |
| 261 | +**利点**: 環境に応じて並行処理数を調整可能。 |
| 262 | + |
| 263 | +**フォールバック**: デフォルト値を用意することで、環境変数が設定されていない場合も動作します。 |
| 264 | + |
| 265 | +```typescript |
| 266 | +const QUEUE_CONCURRENCY = { |
| 267 | + contestTaskPairs: Number(process.env.SEED_CONTEST_TASK_PAIRS_CONCURRENCY) || 2, |
| 268 | +}; |
| 269 | +``` |
| 270 | + |
| 271 | +### 6. ログ出力の重要性 |
| 272 | + |
| 273 | +**ポイント**: |
| 274 | + |
| 275 | +- 処理開始・完了ログで全体的な進捗を把握 |
| 276 | +- エラー発生時は詳細をログ出力 |
| 277 | +- 既存データとの重複は警告またはスキップログを出力 |
| 278 | + |
| 279 | +**効果**: トレーニング・デバッグ時の問題特定が容易 |
0 commit comments