File tree Expand file tree Collapse file tree
docs/dev-notes/2026-02-28/workbook-order
src/features/workbooks/services Expand file tree Collapse file tree Original file line number Diff line number Diff line change @@ -45,3 +45,24 @@ paths:
4545
4646- Use ` prisma.$transaction() ` for multi-step operations
4747- Handle errors with try-catch and proper rollback
48+
49+ ## Idempotent Writes
50+
51+ - Prefer ` createMany({ skipDuplicates: true }) ` over try-catching P2002 when a unique constraint violation is expected (e.g., double-submit race condition). It maps to ` INSERT ... ON CONFLICT DO NOTHING ` and keeps intent clear.
52+ - Constraints: top-level ` createMany ` only (not nested); PostgreSQL, CockroachDB, SQLite only.
53+
54+ ## Validate Constraints
55+
56+ Prisma does not support ` @@check ` in ` schema.prisma ` . To add a validate constraint:
57+
58+ 1 . Run ` pnpm exec prisma migrate dev --create-only --name <description> ` to generate the migration file without applying it
59+ 2 . Edit the generated ` migration.sql ` to add the validate constraint manually
60+ 3 . Run ` pnpm exec prisma migrate dev ` to apply
61+
62+ After adding a validate constraint, add a comment to ` docs/erd.md ` under the relevant entity:
63+
64+ ```
65+ %% XOR constraint: workbookplacement_xor_grade_category — exactly one of taskGrade or solutionCategory must be non-null
66+ ```
67+
68+ This is the only place validate constraints are visible, since Prisma omits them from ` schema.prisma ` .
Original file line number Diff line number Diff line change @@ -120,6 +120,7 @@ XOR 制約は `prisma migrate dev --create-only` で migration ファイルを
120120
121121- バリデーションループに DB 呼び出しが入ったら N+1 を疑い、` findMany({ where: { id: { in: ids } } }) ` + ` Map ` パターンに置き換える
122122- Prisma enum とアプリ enum は構造が同じでも TypeScript は別型として扱う。キャストが必要な箇所を残すこと
123+ - ダブルサブミットなどレースコンディションによる unique 制約違反は ` try-catch ` で P2002 を握りつぶすより ` createMany({ skipDuplicates: true }) ` で冪等な書き込みを DB に委ねる。PostgreSQL/CockroachDB/SQLite のみ対応・nested createMany 不可に注意
123124
124125### Svelte 5
125126
@@ -150,10 +151,6 @@ XOR 制約は `prisma migrate dev --create-only` で migration ファイルを
150151
151152| 教訓 | 一般化先 | ステータス |
152153| ------------------------------------- | ------------------------------------ | ---------- |
153- | ` redirect() ` vs ` error() ` の使い分け | ` .claude/rules/auth.md ` | 実装済み |
154- | features テスト配置コロケーション | ` AGENTS.md ` Project Structure | 実装済み |
155- | DB CHECK 制約 → ERD.md コメント | ` .claude/rules/prisma-db.md ` | 実装済み |
156- | スキル CLI コマンドの事前動作確認 | ` .claude/rules/skills.md ` (新規) | 実装済み |
157154| ` try/finally ` クリーンアップ | ` .claude/rules/testing.md ` | 検討中 |
158155| ` z.number().int() ` 整数バリデーション | ` .claude/rules/testing.md ` | 検討中 |
159156| ` onpointerdown ` も止める(DnD) | ` .claude/rules/svelte-components.md ` | 検討中 |
Original file line number Diff line number Diff line change @@ -214,6 +214,17 @@ describe('createInitialPlacements', () => {
214214 const callArg = vi . mocked ( prisma . workBookPlacement . createMany ) . mock . calls [ 0 ] [ 0 ] ;
215215 expect ( callArg ?. data ) . toHaveLength ( 4 ) ; // 2 curriculum + 2 solution
216216 } ) ;
217+
218+ test ( 'calls createMany with skipDuplicates to tolerate concurrent double-submit' , async ( ) => {
219+ mockWorkBookFindManyOnce ( unplacedCurriculumRows ) ;
220+ mockWorkBookFindManyOnce ( unplacedSolutionWorkbooks ) ;
221+ vi . mocked ( prisma . workBookPlacement . createMany ) . mockResolvedValue ( { count : 4 } ) ;
222+
223+ await createInitialPlacements ( ) ;
224+
225+ const callArg = vi . mocked ( prisma . workBookPlacement . createMany ) . mock . calls [ 0 ] [ 0 ] ;
226+ expect ( callArg ?. skipDuplicates ) . toBe ( true ) ;
227+ } ) ;
217228} ) ;
218229
219230describe ( 'buildTaskMapFromCurriculumRows' , ( ) => {
Original file line number Diff line number Diff line change @@ -89,6 +89,7 @@ export async function createInitialPlacements(): Promise<void> {
8989
9090 await prisma . workBookPlacement . createMany ( {
9191 data : [ ...curriculumPlacements , ...solutionPlacements ] ,
92+ skipDuplicates : true ,
9293 } ) ;
9394}
9495
You can’t perform that action at this time.
0 commit comments