drizzle-orm-expert について
drizzle-orm-expert は、Drizzle ORM で TypeScript のデータベース層を構築する開発者向けのスキルです。スキーマ設計、型付きクエリ、マイグレーション、連携、性能面の手法に加え、アダプター固有の注意点も扱います。
主な機能
- 01TypeScript のスキーマと型推論
- 02リレーショナルおよび SQL 風クエリ
- 03Drizzle Kit のマイグレーション案内
- 04データベースアダプター設定例
- 05性能とドライバーの注意点
活用シーン
- 01スキーマとリレーショナルクエリの設計
- 02データベースマイグレーションの生成と実行
- 03Drizzle と Next.js や tRPC の連携
- 04サーバーレスの接続方式の確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sickn33/ agentic-awesome-skills --skill drizzle-orm-expertインストール後、Claude Code で/drizzle-orm-expertを入力して読み込まれたか確認します。
詳細
Drizzle ORM エキスパート
本番環境での利用に対応した Drizzle ORM のエキスパートです。Drizzle ORM と TypeScript を使って、型安全で高パフォーマンスなデータベースレイヤーを構築する開発者を支援します。スキーマ設計、リレーショナルクエリー API、Drizzle Kit のマイグレーション、Next.js、tRPC、サーバーレスデータベース(Neon、PlanetScale、Turso、Supabase)との統合に精通しています。
このスキルを使う場面
- 新規または既存のプロジェクトで Drizzle ORM をセットアップするとき
- Drizzle の TypeScript ファーストなアプローチでデータベーススキーマを設計するとき
- 複雑なリレーショナルクエリー(結合、サブクエリー、集計)を記述するとき
- Drizzle Kit のマイグレーションをセットアップまたはトラブルシューティングするとき
- Drizzle を Next.js App Router、tRPC、Hono と統合するとき
- データベースのパフォーマンス(プリペアドステートメント、バッチ処理、コネクションプーリング)を最適化するとき
- Prisma、TypeORM、Knex から Drizzle に移行するとき
コアコンセプト
Drizzle を選ぶ理由
Drizzle ORM は、実行時に SQL を生成する TypeScript クエリビルダーを提供します。実行時やドライバーとの互換性、およびオーバーヘッドは、選択したアダプターとデプロイ環境によって異なります。主な特長は次のとおりです。
- SQL に似た API:SQL を知っていれば、Drizzle も使えます
- アダプター固有のデプロイ:Cloudflare Workers、エッジランタイム、Deno では、ドライバーとバンドルを確認してください
- 完全な型推論:スキーマ、型、クエリーはすべてコンパイル時に連携します
- リレーショナルクエリー API:N+1 問題を起こさずに、Prisma のようなネストされた include を利用できます
スキーマ設計パターン
テーブル定義
// db/schema.ts
import { pgTable, text, integer, timestamp, boolean, uuid, pgEnum } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
// Enums
export const roleEnum = pgEnum("role", ["admin", "user", "moderator"]);
// Users table
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
email: text("email").notNull().unique(),
name: text("name").notNull(),
role: roleEnum("role").default("user").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
});
// Posts table with foreign key
export const posts = pgTable("posts", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
content: text("content"),
published: boolean("published").default(false).notNull(),
authorId: uuid("author_id").references(() => users.id, { onDelete: "cascade" }).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
リレーション
// db/relations.ts
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
型推論
// Infer types directly from your schema — no separate type files needed
import type { InferSelectModel, InferInsertModel } from "drizzle-orm";
export type User = InferSelectModel<typeof users>;
export type NewUser = InferInsertModel<typeof users>;
export type Post = InferSelectModel<typeof posts>;
export type NewPost = InferInsertModel<typeof posts>;
スキーマとリレーションの例では、ある世代のリレーショナルクエリー API を使用しています。インストール済みの Drizzle のバージョンに対応していることを確認してください。db/relations.ts には明示的なインポートが必要で、そのエクスポートを drizzle に渡すスキーマに含める必要があります。updatedAt の defaultNow は初期化時にのみ値を設定し、更新時に自動変更されるわけではありません。ドライバーによってサポート内容は異なります。neon-http は以下に示すインタラクティブなトランザクションコールバックをサポートしておらず、db.batch、プリペアドステートメント、returning のサポートもアダプターによって異なります。依存する書き込みをアトミックに行う必要がある場合は、対応するトランザクション対応ドライバー、またはドキュメントに記載された非インタラクティブなバッチ処理を選んでください。マイグレーション SQL は、バックアップ済みの対象に対して事前に確認してください。生成された SQL がデータの安全な保持を保証するわけではありません。
クエリパターン
Select クエリー(SQL に似た API)
import { eq, and, like, desc, count, sql } from "drizzle-orm";
// Basic select
const allUsers = await db.select().from(users);
// Filtered with conditions
const admins = await db.select().from(users).where(eq(users.role, "admin"));
// Partial select (only specific columns)
const emails = await db.select({ email: users.email }).from(users);
// Join query
const postsWithAuthors = await db
.select({
title: posts.title,
authorName: users.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(10);
// Aggregation
const postCounts = await db
.select({
authorId: posts.authorId,
postCount: count(posts.id),
})
.from(posts)
.groupBy(posts.authorId);
リレーショナルクエリー(Prisma に似た API)
// Nested includes — Drizzle resolves in a single query
const usersWithPosts = await db.query.users.findMany({
with: {
posts: {
where: eq(posts.published, true),
orderBy: [desc(posts.createdAt)],
limit: 5,
},
},
});
// Find one with nested data
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: { posts: true },
});
挿入、更新、削除
// Insert with returning
const [newUser] = await db
.insert(users)
.values({ email: "[email protected]", name: "Dev" })
.returning();
// Batch insert
await db.insert(posts).values([
{ title: "Post 1", authorId: newUser.id },
{ title: "Post 2", authorId: newUser.id },
]);
// Update
await db.update(users).set({ name: "Updated" }).where(eq(users.id, userId));
// Delete
await db.delete(posts).where(eq(posts.authorId, userId));
トランザクション
const result = await db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({ email, name }).returning();
await tx.insert(posts).values({ title: "Welcome Post", authorId: user.id });
return user;
});
マイグレーションのワークフロー(Drizzle Kit)
設定
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
});
コマンド
# Generate migration SQL from schema changes
npx drizzle-kit generate
# Push schema directly to database (development only — skips migration files)
npx drizzle-kit push
# Run pending migrations (production)
npx drizzle-kit migrate
# Open Drizzle Studio (GUI database browser)
npx drizzle-kit studio
データベースクライアントのセットアップ
PostgreSQL(Neon Serverless)
// db/index.ts
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
SQLite(Turso/LibSQL)
import { drizzle } from "drizzle-orm/libsql";
import { createClient } from "@libsql/client";
import * as schema from "./schema";
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN,
});
export const db = drizzle(client, { schema });
MySQL(PlanetScale)
import { drizzle } from "drizzle-orm/planetscale-serverless";
import { Client } from "@planetscale/database";
import * as schema from "./schema";
const client = new Client({ url: process.env.DATABASE_URL! });
export const db = drizzle(client, { schema });
パフォーマンスの最適化
プリペアドステートメント
// Prepare once, execute many times
const getUserById = db.query.users
.findFirst({
where: eq(users.id, sql.placeholder("id")),
})
.prepare("get_user_by_id");
// Execute with parameters
const user = await getUserById.execute({ id: "abc-123" });
バッチ処理
// Use db.batch() for multiple independent queries in one round-trip
const [allUsers, recentPosts] = await db.batch([
db.select().from(users),
db.select().from(posts).orderBy(desc(posts.createdAt)).limit(10),
]);
スキーマでのインデックス設定
import { index, uniqueIndex } from "drizzle-orm/pg-core";
export const posts = pgTable(
"posts",
{
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
authorId: uuid("author_id").references(() => users.id).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
index("posts_author_idx").on(table.authorId),
index("posts_created_idx").on(table.createdAt),
]
);
Next.js との統合
サーバーコンポーネントでの使用
// app/users/page.tsx (React Server Component)
import { db } from "@/db";
import { users } from "@/db/schema";
export default async function UsersPage() {
const allUsers = await db.select().from(users);
return (
<ul>
{allUsers.map((u) => (
<li key={u.id}>{u.name}</li>
))}
</ul>
);
}
この最小限のアクションは不完全です。呼び出し元を認証・認可し、FormData の値(null または File の場合があります)を検証してください。挿入前に、長さやメール形式を制限し、重複やエラーを処理してください。TypeScript の型アサーションは、実行時の検証ではありません。信頼できない入力からアプリケーションのロールを決して受け取らないでください。
サーバーアクション
// app/actions.ts
"use server";
import { db } from "@/db";
import { users } from "@/db/schema";
export async function createUser(formData: FormData) {
const name = formData.get("name") as string;
const email = formData.get("email") as string;
await db.insert(users).values({ name, email });
}
ベストプラクティス
- ✅ 実施すること: スキーマ定義はすべて単一の
db/schema.tsにまとめるか、ドメインごとに分割します(db/schema/users.ts、db/schema/posts.tsなど)。 - ✅ 実施すること: 手動でインターフェイスを定義するのではなく、型安全性のために
InferSelectModelとInferInsertModelを使用します。 - ✅ 実施すること: N+1 問題を避けるため、ネストされたデータにはリレーショナルクエリー API(
db.query.*)を使用します。 - ✅ 実施すること: 本番環境で頻繁に実行されるクエリーには、プリペアドステートメントを使用します。
- ✅ 実施すること: 本番環境では
drizzle-kit generateとmigrateを使用します(pushは決して使用しないでください)。 - ✅ 実施すること: リレーショナルクエリー API を有効にするため、
drizzle()に{ schema }を渡します。 - ❌ 避けること: 本番環境で
drizzle-kit pushを使用しないでください。データが失われるおそれがあります。 - ❌ 避けること: Drizzle クエリービルダーで操作できる場合に、生の SQL を記述しないでください。
- ❌ 避けること:
withを指定してdb.query.*を使用する場合は、relations()の定義を忘れないでください。 - ❌ 避けること: サーバーレス環境でリクエストごとに新しいデータベース接続を作成しないでください。接続プーリングを使用します。
トラブルシューティング
問題: db.query.tableName が未定義です
解決策: リレーションを含むすべてのスキーマオブジェクトを drizzle() に渡します:drizzle(client, { schema })
問題: スキーマの変更後にマイグレーションが競合します
解決策: npx drizzle-kit generate を実行して新しいマイグレーションを作成し、その後 npx drizzle-kit migrate を実行します。
問題: MySQL で .returning() を使用すると型エラーが発生します
解決策: MySQL は RETURNING をサポートしていません。代わりに .execute() を使用し、結果から insertId を読み取ります。
制限事項
- このスキルは、タスクが上記の説明範囲に明確に該当する場合にのみ使用してください。
- 出力を、環境固有の検証、テスト、または専門家によるレビューの代わりとして扱わないでください。
- 必要な入力、権限、安全上の境界、または成功基準が不足している場合は、作業を中断して確認してください。