database-layer について
database-layerは、アプリのデータベース層を構築または改善する開発者向けのスキルです。既存スタックの調査から始まり、スキーマ、マイグレーション、型安全なクライアント、クエリ、性能確認を扱います。
主な機能
- 01既存のデータベースとORMを調査
- 02スキーマ設計と接続設定
- 03マイグレーションの作成と確認
- 04型安全なクライアントの生成
- 05クエリ分析と性能最適化
- 06データベース実装の検査
活用シーン
- 01新規アプリのデータベース層構築
- 02既存データベーススキーマの移行
- 03N+1クエリやインデックス不足の調査
- 04マイグレーションと設定の検証
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ mgd34msu/ goodvibes-plugin --skill database-layerインストール後、Claude Code で/database-layerを入力して読み込まれたか確認します。
同梱ファイルreferences/orm-comparison.mdscripts/database-checklist.sh
詳細
リソース
scripts/
database-checklist.sh
references/
orm-comparison.md
データベース層の実装
このスキルでは、初期スキーマの設計からクエリの最適化まで、アプリケーションのデータベース層の実装方法を説明します。GoodVibes の高精度ツールとプロジェクト分析ツールを活用して、型安全で本番環境に対応したデータベースを実装できます。
このスキルを使用する場面
次の作業が必要な場合に、このスキルを使用します。
- 新しいデータベース接続と ORM を設定する
- データベーススキーマを設計、実装する
- マイグレーションを作成、実行する
- 型安全なデータベースクライアントを生成する
- クエリを作成し、リレーションシップを扱う
- データベースのパフォーマンスを最適化する
- 既存のデータベースインフラストラクチャと統合する
ワークフロー
データベース層を実装する際は、次の手順に従います。
1. 既存のデータベースインフラストラクチャを確認する
データベースに変更を加える前に、detect_stack 分析ツールを使って現在の状態を確認します。
detect_stack:
project_root: "."
categories: ["database", "orm"]
次の情報を特定できます。
- 既存のデータベース技術(PostgreSQL、MySQL、MongoDB、SQLite)
- 使用中の ORM/クエリビルダー(Prisma、Drizzle、Kysely、Mongoose)
- スキーマ定義ファイル
- マイグレーションツール
- 接続管理のパターン
データベースに関する決定事項をプロジェクトメモリで確認します。
precision_read:
files:
- path: ".goodvibes/memory/decisions.json"
- path: ".goodvibes/memory/patterns.json"
verbosity: minimal
次の情報を探します。
- 過去に選択したデータベース技術(「型安全性のために Prisma を使用する」)
- マイグレーション戦略(「常にロールバック可能なマイグレーションを使用する」)
- パフォーマンスのパターン(「外部キーにインデックスを追加する」)
- 既知の問題(「ユーザーエンドポイントで N+1 クエリを避ける」)
すでにデータベースがある場合は、現在のスキーマを把握します。
get_database_schema:
project_root: "."
include_relations: true
include_indexes: true
次の情報が返されます。
- テーブル/コレクションの定義
- カラムの型と制約
- リレーションシップ(外部キー、参照)
- インデックスと一意制約
- 列挙型とカスタム型
2. データベース技術を選択する
新規に開始する場合は、ORM 比較リファレンスを参照して、適切な技術スタックを選択します。
参照:意思決定ツリーは references/orm-comparison.md をご覧ください。
主な判断要素:
| 要素 | 推奨 |
|---|---|
| 型安全性を重視 | Prisma または Drizzle |
| SQL の制御を最大限に重視 | Kysely または Drizzle |
| ドキュメントデータベース | Mongoose(MongoDB) |
| サーバーレス/エッジ | libSQL/Turso と Drizzle |
| 既存の PostgreSQL | Prisma または Drizzle |
| 学習コスト | Prisma(DX が最良) |
決定事項をメモリに記録します。
選択した後、今後参照できるよう、.goodvibes/memory/decisions.json に決定事項を記録します。
3. スキーマを設計する
まずエンティティとリレーションシップを特定します。
Entities: User, Post, Comment, Category
Relationships:
- User 1:N Post (author)
- Post N:M Category (through PostCategory)
- Post 1:N Comment
- User 1:N Comment (author)
precision_write を使ってスキーマファイルを作成します。
Prisma の場合:
precision_write:
files:
- path: "prisma/schema.prisma"
content: |
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
title String
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
categories Category[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([authorId])
@@index([published, createdAt])
}
model Category {
id String @id @default(cuid())
name String @unique
posts Post[]
}
model Comment {
id String @id @default(cuid())
content String
post Post @relation(fields: [postId], references: [id])
postId String
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
@@index([postId])
@@index([authorId])
}
verbosity: minimal
Drizzle の場合:
precision_write:
files:
- path: "src/db/schema.ts"
content: |
import { pgTable, text, timestamp, boolean, index } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
export const users = pgTable('users', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
title: text('title').notNull(),
content: text('content').notNull(),
published: boolean('published').default(false).notNull(),
authorId: text('author_id').notNull().references(() => users.id),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
}, (table) => ({
authorIdx: index('author_idx').on(table.authorId),
publishedCreatedIdx: index('published_created_idx').on(table.published, table.createdAt),
}));
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
verbosity: minimal
スキーマ設計のベストプラクティス:
-
適切な ID 戦略を使用します。
- 分散システムには CUID/UUID
- シンプルなアプリには自動採番
- 結合テーブルには複合キー
-
タイムスタンプを追加します。
- 常に
createdAtを含めます - 変更可能なエンティティには
updatedAtを含めます - 論理削除には
deletedAtを検討します
- 常に
-
適切にインデックスを設定します。
- 外部キー(結合用)
- 頻繁にクエリされるフィールド
- 複数カラムでのフィルタリング用の複合インデックス
- 該当する場合は一意制約
-
スケールを考慮して計画します。
- TEXT と VARCHAR の制限
- 柔軟なデータには JSONB(PostgreSQL)
- 大きなテキスト/BLOB には別テーブルを使用
4. データベース接続を設定する
環境設定を作成します。
precision_write:
files:
- path: ".env.example"
content: |
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/dbname"
# For Prisma with connection pooling
# DATABASE_URL="postgresql://user:password@localhost:5432/dbname?pgbouncer=true"
# DIRECT_URL="postgresql://user:password@localhost:5432/dbname"
mode: overwrite
verbosity: minimal
データベースクライアントのモジュールを作成します。
Prisma の場合:
precision_write:
files:
- path: "src/lib/db.ts"
content: |
import { PrismaClient } from '@prisma/client';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const db =
globalForPrisma.prisma ??
new PrismaClient({
log:
process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = db;
}
verbosity: minimal
Drizzle の場合:
precision_write:
files:
- path: "src/lib/db.ts"
content: |
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from '@/db/schema';
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString, {
max: process.env.NODE_ENV === 'production' ? 10 : 1,
});
export const db = drizzle(client, { schema });
verbosity: minimal
5. マイグレーションを実行する
precision_exec を使用して、期待される結果を指定しながらマイグレーションコマンドを実行します。
Prisma の場合:
precision_exec:
commands:
- cmd: "npx prisma migrate dev --name init"
timeout_ms: 60000
expect:
exit_code: 0
# Note: Prisma outputs progress to stderr; this is expected behavior
- cmd: "npx prisma generate"
expect:
exit_code: 0
verbosity: standard
Drizzle の場合:
precision_exec:
commands:
- cmd: "npx drizzle-kit generate"
expect:
exit_code: 0
- cmd: "npx drizzle-kit push"
timeout_ms: 60000
expect:
exit_code: 0
verbosity: standard
マイグレーションのベストプラクティス:
- 適用する前に、生成されたマイグレーションを必ず確認します。
- ロールバック可能なマイグレーションを使用します(up と down の両方を含めます)。
- 本番環境に適用する前に、開発用データベースでマイグレーションをテストします。
- マイグレーションを実行する前に、本番データをバックアップします。
- 複数ステップのマイグレーションにはトランザクションを使用します。
6. 型安全なクライアントを生成する
generate_types プロジェクトツールを使って、スキーマから TypeScript の型を生成します。
generate_types:
project_root: "."
source: "database"
output_path: "src/types/db.ts"
次の型定義が作成されます。
- テーブル/コレクションの型
- 挿入型(自動生成フィールドを除外)
- 更新型(すべてのフィールドが省略可能)
- 選択型(リレーションを含む)
型生成を確認します。
precision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
verbosity: minimal
7. クエリを実装する
まず、基本的な CRUD 操作から始めます。
precision_write:
files:
- path: "src/db/queries/users.ts"
content: |
import { db } from '@/lib/db';
export async function createUser(data: { email: string; name?: string }) {
return db.user.create({
data,
});
}
export async function getUserById(id: string) {
return db.user.findUnique({
where: { id },
include: {
posts: true,
},
});
}
export async function updateUser(
id: string,
data: { email?: string; name?: string }
) {
return db.user.update({
where: { id },
data,
});
}
export async function deleteUser(id: string) {
return db.user.delete({
where: { id },
});
}
verbosity: minimal
プロジェクトツールを使って N+1 クエリのパターンを確認します。
get_prisma_operations:
project_root: "."
analyze_performance: true
次の項目を特定します。
- N+1 クエリが発生する可能性のある箇所(
includeまたはselectの不足) - 頻繁にクエリされるフィールドに対するインデックスの不足
- 非効率なリレーションの読み込み
クエリを最適化します。
-
selectを使ってフィールドを制限します。db.user.findMany({ select: { id: true, email: true }, // Don't fetch unused fields }); -
リレーションを先読みします。
db.post.findMany({ include: { author: true }, // Prevents N+1 }); -
ページネーションを使います。
db.post.findMany({ take: 20, skip: (page - 1) * 20, }); -
データベースレベルの制約を追加します。
@@index([userId, createdAt(sort: Desc)])
8. トランザクションを実装する
複数ステップの操作には、トランザクションを使います。
Prisma:
export async function createPostWithCategories(
postData: { title: string; content: string; authorId: string },
categoryIds: string[]
) {
return db.$transaction(async (tx) => {
const post = await tx.post.create({
data: {
...postData,
categories: {
connect: categoryIds.map((id) => ({ id })),
},
},
});
await tx.user.update({
where: { id: postData.authorId },
data: { updatedAt: new Date() },
});
return post;
});
}
Drizzle:
export async function createPostWithCategories(
postData: { title: string; content: string; authorId: string },
categoryIds: string[]
) {
return db.transaction(async (tx) => {
const [post] = await tx.insert(posts).values(postData).returning();
await tx.insert(postCategories).values(
categoryIds.map((categoryId) => ({
postId: post.id,
categoryId,
}))
);
return post;
});
}
9. 開発用データをシードする
ローカル開発用のシードスクリプトを作成します。
precision_write:
files:
- path: "prisma/seed.ts"
content: |
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// Clear existing data
await prisma.comment.deleteMany();
await prisma.post.deleteMany();
await prisma.user.deleteMany();
await prisma.category.deleteMany();
// Create users
const alice = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Alice',
},
});
const bob = await prisma.user.create({
data: {
email: '[email protected]',
name: 'Bob',
},
});
// Create categories
const tech = await prisma.category.create({
data: { name: 'Technology' },
});
const news = await prisma.category.create({
data: { name: 'News' },
});
// Create posts
await prisma.post.create({
data: {
title: 'First Post',
content: 'This is the first post',
published: true,
authorId: alice.id,
categories: {
connect: [{ id: tech.id }],
},
},
});
console.log('Database seeded successfully');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
verbosity: minimal
package.json を更新します。
precision_edit:
edits:
- path: "package.json"
find: '"scripts": {'
hints:
near_line: 2
replace: |
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"scripts": {
verbosity: minimal
10. 実装を検証する
データベースのチェックリストスクリプトを実行します。
./plugins/goodvibes/skills/outcome/database-layer/scripts/database-checklist.sh .
次の項目を検証します。
- スキーマファイルが存在し、有効であること
- マイグレーションディレクトリが存在すること
- データベース URL が .env.example に記載されていること
- 型生成が設定されていること
- SQL インジェクションの脆弱性(文字列連結)がないこと
- コネクションプーリングが設定されていること
- 外部キーにインデックスが設定されていること
型チェックとテストを実行します。
precision_exec:
commands:
- cmd: "npm run typecheck"
expect:
exit_code: 0
- cmd: "npm run test -- db"
expect:
exit_code: 0
verbosity: minimal
query_database を使ってデータの整合性を確認します。
query_database:
project_root: "."
query: "SELECT COUNT(*) FROM users;"
よくあるパターン
論理削除
deletedAt フィールドを追加し、クエリで絞り込みます。
model Post {
id String @id
deletedAt DateTime?
}
// Soft delete
await db.post.update({
where: { id },
data: { deletedAt: new Date() },
});
// Query only active records
await db.post.findMany({
where: { deletedAt: null },
});
楽観的ロック
バージョンフィールドを使って、同時更新を防ぎます。
model Post {
id String @id
version Int @default(0)
}
await db.post.update({
where: {
id: postId,
version: currentVersion,
},
data: {
title: newTitle,
version: { increment: 1 },
},
});
コネクションプーリング
サーバーレス環境では、コネクションプーリングを使います。
# PgBouncer
DATABASE_URL="postgresql://user:password@localhost:6543/db?pgbouncer=true"
DIRECT_URL="postgresql://user:password@localhost:5432/db"
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
全文検索
PostgreSQL:
@@index([content(ops: raw("gin_trgm_ops"))], type: Gin)
await db.$queryRaw`
SELECT * FROM posts
WHERE to_tsvector('english', content) @@ to_tsquery('search terms')
`;
セキュリティチェックリスト
- データベースの認証情報を環境変数に設定する(コミットしない)
- ユーザーが提供するすべてのデータを入力検証する
- パラメーター化クエリを使う(文字列連結をしない)
- マルチテナントアプリでは行レベルセキュリティを設定する
- 高コストなクエリにレート制限を設ける
- 機密性の高い操作を監査ログに記録する
- データベースユーザーの権限を最小限にする
- 本番環境のデータベース接続に SSL/TLS を使う
パフォーマンスチェックリスト
- 外部キーにインデックスを設定する
- 複数列のフィルターには複合インデックスを設定する
- コネクションプーリングを設定する
- クエリ結果にページネーションを適用する
- N+1 クエリを防ぐため、リレーションを先読みする
- 開発環境でデータベースクエリをログに記録する
- 遅いクエリには Explain/Analyze を実行する
- 頻繁にアクセスされるデータをキャッシュする
トラブルシューティング
制約違反によりマイグレーションが失敗する
- 既存データが新しい制約に抵触していないか確認します
- スキーマのマイグレーション前にデータマイグレーションを追加します
- 複数段階のマイグレーションを使います(列を NULL 許容で追加し、データを設定してから必須にする)
N+1 クエリが検出された
get_prisma_operationsを使って発生箇所を特定します- リレーションを指定した
includeまたはselectを追加します - 複雑なケースでは
dataloaderの使用を検討します
コネクションプールが枯渇する
- 接続文字列のプールサイズを増やします(
?pool_timeout=10) awaitの不足(接続が解放されていない)がないか確認します- コネクションプーラー(PgBouncer、Prisma Accelerate)を使います
型生成が失敗する
npx prisma validateでスキーマ構文を検証します- 生成されたファイルを削除してから再生成します
- リレーションに循環依存がないか確認します
次のステップ
データベースレイヤーを実装した後は、次の作業を行います。
- キャッシュを追加する - 頻繁にアクセスされるデータには Redis を使用します
- 検索を実装する - 全文検索または Elasticsearch を追加します
- 監視を追加する - クエリのパフォーマンスと遅いクエリを追跡します
- テストを作成する - クエリのユニットテストとトランザクションの統合テストを作成します
- スキーマを文書化する - チームで参照できるよう、スキーマにコメントを追加します
- バックアップを計画する - データベースの自動バックアップを設定します
参考資料や判断フローチャートについては、次を参照してください。
- references/orm-comparison.md - ORM の選定ガイド
- scripts/database-checklist.sh - 検証スクリプト