schema-builder について
schema-builderは、データベース構造を作成・変更するConvex開発者向けのスキーマ設計スキルです。リレーション、インデックス設計、厳密なバリデーターに重点を置き、ネストしたデータをリレーショナル構造へ移行する方法も示します。
主な機能
- 01Convexスキーマの作成と拡張
- 02一対多・多対多リレーションの設計
- 03単一フィールド・複合インデックスの設計
- 04厳密な型バリデーターの適用
- 05ネストデータのリレーショナル構造への変換
活用シーン
- 01convex/schema.tsの新規作成
- 02既存テーブルへのインデックス追加
- 03ユーザーとプロジェクトのデータ関係設計
- 04ネストしたレコードの関連テーブルへの分割
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ get-convex/ agent-skills --skill schema-builderインストール後、Claude Code で/schema-builderを入力して読み込まれたか確認します。
詳細
Convex スキーマビルダー
リレーションシップ、インデックス、バリデーターに関するベストプラクティスに従って、適切に構造化された Convex スキーマを作成します。
使用する場面
- 新しい
convex/schema.tsファイルを作成するとき - 既存のスキーマにテーブルを追加するとき
- データモデルのリレーションシップを設計するとき
- インデックスを追加または最適化するとき
- ネストされたデータをリレーショナル構造に変換するとき
スキーマ設計の原則
- ドキュメントとリレーショナルの併用:深いネストではなく、ID 参照を使ったフラットなドキュメントを使用します
- 外部キーにインデックスを設定:検索に使うフィールド(userId、teamId など)には必ずインデックスを設定します
- 配列を制限:配列は小規模で上限が定められたコレクション(8192 件未満)にのみ使用します
- 型安全性:
v.*型を使った厳密なバリデーターを使用します
スキーマのテンプレート
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
export default defineSchema({
tableName: defineTable({
// Required fields
field: v.string(),
// Optional fields
optional: v.optional(v.number()),
// Relations (use IDs)
userId: v.id("users"),
// Enums with union + literal
status: v.union(
v.literal("active"),
v.literal("pending"),
v.literal("archived")
),
// Timestamps
createdAt: v.number(),
updatedAt: v.optional(v.number()),
})
// Index for queries by this field
.index("by_user", ["userId"])
// Compound index for common query patterns
.index("by_user_and_status", ["userId", "status"])
// Index for time-based queries
.index("by_created", ["createdAt"]),
});
よく使われるパターン
1 対多のリレーションシップ
export default defineSchema({
users: defineTable({
name: v.string(),
email: v.string(),
}).index("by_email", ["email"]),
posts: defineTable({
userId: v.id("users"),
title: v.string(),
content: v.string(),
}).index("by_user", ["userId"]),
});
中間テーブルを使った多対多のリレーションシップ
export default defineSchema({
users: defineTable({
name: v.string(),
}),
projects: defineTable({
name: v.string(),
}),
projectMembers: defineTable({
userId: v.id("users"),
projectId: v.id("projects"),
role: v.union(v.literal("owner"), v.literal("member")),
})
.index("by_user", ["userId"])
.index("by_project", ["projectId"])
.index("by_project_and_user", ["projectId", "userId"]),
});
階層データ
export default defineSchema({
comments: defineTable({
postId: v.id("posts"),
parentId: v.optional(v.id("comments")), // null for top-level
userId: v.id("users"),
text: v.string(),
})
.index("by_post", ["postId"])
.index("by_parent", ["parentId"]),
});
小規模で上限が定められた配列(使用可能)
export default defineSchema({
users: defineTable({
name: v.string(),
// Small, bounded collections are fine
roles: v.array(v.union(
v.literal("admin"),
v.literal("editor"),
v.literal("viewer")
)),
tags: v.array(v.string()), // e.g., max 10 tags
}),
});
バリデーターのリファレンス
// Primitives
v.string()
v.number()
v.boolean()
v.null()
v.id("tableName")
// Optional
v.optional(v.string())
// Union types (enums)
v.union(v.literal("a"), v.literal("b"))
// Objects
v.object({
key: v.string(),
nested: v.number(),
})
// Arrays
v.array(v.string())
// Records (arbitrary keys)
v.record(v.string(), v.boolean())
// Any (avoid if possible)
v.any()
インデックス戦略
-
単一フィールドのインデックス:単純な検索に使用します
by_user: ["userId"]by_email: ["email"]
-
複合インデックス:絞り込みクエリーに使用します
by_user_and_status: ["userId", "status"]by_team_and_created: ["teamId", "createdAt"]
-
重複を削除:
by_a_and_bは通常、by_aをカバーします
チェックリスト
- すべての外部キーにインデックスが設定されている
- 一般的なクエリーパターンに複合インデックスが設定されている
- 配列が小規模で上限が定められている(またはリレーションシップに変換されている)
- すべてのフィールドに適切なバリデーターが設定されている
- 列挙型に
v.union(v.literal(...))パターンを使用している - タイムスタンプに
v.number()(エポックからのミリ秒)を使用している
ネスト構造からリレーショナル構造への移行
ネストされた構造から変換する場合:
変換前:
users: defineTable({
posts: v.array(v.object({
title: v.string(),
comments: v.array(v.object({ text: v.string() })),
})),
})
変換後:
users: defineTable({
name: v.string(),
}),
posts: defineTable({
userId: v.id("users"),
title: v.string(),
}).index("by_user", ["userId"]),
comments: defineTable({
postId: v.id("posts"),
text: v.string(),
}).index("by_post", ["postId"]),
コメント
Markdown 対応