Skip to content

Commit 5fce5d8

Browse files
authored
Merge pull request #2735 from AtCoder-NoviSteps/#2734
feat: Add contest task pairs to seeds (#2734)
2 parents 5af34ef + a02f75a commit 5fce5d8

File tree

4 files changed

+494
-0
lines changed

4 files changed

+494
-0
lines changed
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
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+
**効果**: トレーニング・デバッグ時の問題特定が容易

prisma/contest_task_pairs.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
export const contest_task_pairs = [
2+
{
3+
contest_id: 'tessoku-book',
4+
problem_id: 'typical90_s',
5+
problem_index: 'C18',
6+
},
7+
{
8+
contest_id: 'tessoku-book',
9+
problem_id: 'math_and_algorithm_ac',
10+
problem_index: 'C09',
11+
},
12+
{
13+
contest_id: 'tessoku-book',
14+
problem_id: 'abc007_3',
15+
problem_index: 'B63',
16+
},
17+
{
18+
contest_id: 'tessoku-book',
19+
problem_id: 'math_and_algorithm_ap',
20+
problem_index: 'B28',
21+
},
22+
{
23+
contest_id: 'tessoku-book',
24+
problem_id: 'dp_a',
25+
problem_index: 'B16',
26+
},
27+
{
28+
contest_id: 'tessoku-book',
29+
problem_id: 'math_and_algorithm_al',
30+
problem_index: 'B07',
31+
},
32+
{
33+
contest_id: 'tessoku-book',
34+
problem_id: 'typical90_a',
35+
problem_index: 'A77',
36+
},
37+
{
38+
contest_id: 'tessoku-book',
39+
problem_id: 'math_and_algorithm_an',
40+
problem_index: 'A63',
41+
},
42+
{
43+
contest_id: 'tessoku-book',
44+
problem_id: 'math_and_algorithm_am',
45+
problem_index: 'A62',
46+
},
47+
{
48+
contest_id: 'tessoku-book',
49+
problem_id: 'math_and_algorithm_bn',
50+
problem_index: 'A39',
51+
},
52+
{
53+
contest_id: 'tessoku-book',
54+
problem_id: 'math_and_algorithm_aq',
55+
problem_index: 'A29',
56+
},
57+
{
58+
contest_id: 'tessoku-book',
59+
problem_id: 'math_and_algorithm_o',
60+
problem_index: 'A27',
61+
},
62+
{
63+
contest_id: 'tessoku-book',
64+
problem_id: 'math_and_algorithm_ai',
65+
problem_index: 'A06',
66+
},
67+
];

0 commit comments

Comments
 (0)