Jotai完全ガイド 2025 - アトミック状態管理の新時代
Jotai v2を徹底解説。アトミックアプローチによる状態管理の革新、primitive・derived atoms、React 18対応、TypeScript完全サポート、実践的なパターンまで、モダンなReactアプリケーション開発の全てを網羅します。
Drizzle ORMは、TypeScriptファーストで設計された軽量かつ型安全なORMです。Prismaとの比較を交えながら、マイグレーション、スキーマ定義、クエリビルダーなど、実践的な使い方を詳しく解説します。
Typescript プロジェクトでデータベースを扱う際、型安全性とパフォーマンスの両立に悩んでいませんか? Drizzle ORM は、軽量でありながら強力な型推論を提供する新世代の ORM です。 本記事では、Prisma との比較を含め、実践的な Drizzle ORM の使い方を徹底解説します。
2025 年、typescript は標準となり、型安全性は開発の必須要件となりました。 Drizzle ORM は、この要求に応える次世代の ORM として設計されています。
チャートを読み込み中...
特徴 | Drizzle | Prisma | TypeORM |
---|---|---|---|
バンドルサイズ | 50KB | 500KB+ | 300KB+ |
型安全性 | 完全 | 高い | 中程度 |
エッジ対応 | ◎ | △ | × |
パフォーマンス | 高速 | 標準 | 標準 |
学習曲線 | 緩やか | 中程度 | 急 |
Drizzle は「sql を隠さない」設計思想を持っています。 sql の知識を活かしながら、型安全性を享受できます。
Drizzle のセットアップは驚くほどシンプルです。
# Drizzle ORM本体
bun add drizzle-orm
# PostgreSQLを使用する場合
bun add postgres
bun add -d drizzle-kit @types/pg
# MySQLを使用する場合
bun add mysql2
bun add -d drizzle-kit
# SQLiteを使用する場合
bun add better-sqlite3
bun add -d drizzle-kit @types/better-sqlite3
// db/index.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString);
export const db = drizzle(client, { schema });
// db/index.ts
import { drizzle } from 'drizzle-orm/mysql2';
import mysql from 'mysql2/promise';
import * as schema from './schema';
const connection = await mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
});
export const db = drizzle(connection, { schema });
// db/index.ts
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
const sqlite = new Database('sqlite.db');
export const db = drizzle(sqlite, { schema });
// Cloudflare Workers (D1)
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';
export default {
async fetch(request: Request, env: Env) {
const db = drizzle(env.DB, { schema });
// クエリ実行
}
};
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
Drizzle のスキーマ定義は、typescript のコードとして記述します。
// schema.ts
import {
pgTable,
serial,
text,
timestamp,
boolean,
integer,
uuid
} from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').unique().notNull(),
username: text('username').unique().notNull(),
passwordHash: text('password_hash').notNull(),
isActive: boolean('is_active').default(true),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: uuid('id').defaultRandom().primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id')
.references(() => users.id)
.notNull(),
publishedAt: timestamp('published_at'),
createdAt: timestamp('created_at').defaultNow(),
});
// インデックスとチェック制約
export const products = pgTable('products', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
price: integer('price').notNull(),
stock: integer('stock').default(0),
}, (table) => {
return {
nameIdx: index('name_idx').on(table.name),
priceCheck: check('price_check', sql`${table.price} > 0`),
};
});
// 複合主キーと複合インデックス
export const userRoles = pgTable('user_roles', {
userId: integer('user_id').references(() => users.id),
roleId: integer('role_id').references(() => roles.id),
assignedAt: timestamp('assigned_at').defaultNow(),
}, (table) => {
return {
pk: primaryKey(table.userId, table.roleId),
userIdx: index('user_idx').on(table.userId),
};
});
// relations.ts
import { relations } from 'drizzle-orm';
import { users, posts, comments } from './schema';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
comments: many(comments),
}));
export const postsRelations = relations(posts, ({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
comments: many(comments),
}));
export const commentsRelations = relations(comments, ({ one }) => ({
post: one(posts, {
fields: [comments.postId],
references: [posts.id],
}),
author: one(users, {
fields: [comments.authorId],
references: [users.id],
}),
}));
Drizzle のクエリビルダーは、sql の知識を直接活かせる設計です。
// SELECT
const allUsers = await db.select().from(users);
// WHERE条件
const activeUsers = await db
.select()
.from(users)
.where(eq(users.isActive, true));
// JOIN
const postsWithAuthors = await db
.select({
postId: posts.id,
postTitle: posts.title,
authorName: users.username,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
// 集計
const userCount = await db
.select({ count: count() })
.from(users);
// 複雑な条件
const filteredPosts = await db
.select()
.from(posts)
.where(
and(
eq(posts.authorId, userId),
or(
like(posts.title, '%TypeScript%'),
like(posts.content, '%Drizzle%')
),
gte(posts.publishedAt, new Date('2025-01-01'))
)
)
.orderBy(desc(posts.createdAt))
.limit(10)
.offset(20);
// サブクエリ
const topAuthors = db
.select({
authorId: posts.authorId,
postCount: count(posts.id).as('post_count'),
})
.from(posts)
.groupBy(posts.authorId)
.having(gte(count(posts.id), 5))
.as('top_authors');
const result = await db
.select()
.from(users)
.innerJoin(topAuthors, eq(users.id, topAuthors.authorId));
// INSERT
const newUser = await db
.insert(users)
.values({
email: 'user@example.com',
username: 'newuser',
passwordHash: hashedPassword,
})
.returning();
// バルクINSERT
const newPosts = await db
.insert(posts)
.values([
{ title: 'Post 1', content: 'Content 1', authorId: 1 },
{ title: 'Post 2', content: 'Content 2', authorId: 1 },
])
.returning({ id: posts.id });
// UPDATE
const updatedUser = await db
.update(users)
.set({ username: 'updated_username' })
.where(eq(users.id, userId))
.returning();
// DELETE
await db
.delete(posts)
.where(
and(
eq(posts.authorId, userId),
lt(posts.createdAt, thirtyDaysAgo)
)
);
Drizzle は、スキーマから自動的に型を推論します。 IDE の補完機能が完璧に動作し、型エラーをコンパイル時に検出できます。
Drizzle Kit は、スキーマの変更を管理する強力なツールです。
# マイグレーションファイルを生成
bun drizzle-kit generate:pg
# 生成されたマイグレーションを確認
bun drizzle-kit migrate
# SQLを直接実行せずに確認
bun drizzle-kit push:pg --dry-run
スキーマを直接同期
マイグレーションファイル生成
レビュー後に適用
必要に応じて戻す
// マイグレーション実行
import { migrate } from 'drizzle-orm/postgres-js/migrator';
import { db } from './db';
await migrate(db, { migrationsFolder: './drizzle' });
// カスタムマイグレーション
const customMigration = async () => {
await db.execute(sql`
CREATE INDEX CONCURRENTLY idx_posts_published
ON posts(published_at)
WHERE published_at IS NOT NULL;
`);
};
// 基本的なトランザクション
const result = await db.transaction(async (tx) => {
const user = await tx
.insert(users)
.values({ email, username, passwordHash })
.returning();
await tx
.insert(userProfiles)
.values({
userId: user[0].id,
bio: 'New user',
});
return user[0];
});
// ロールバック
try {
await db.transaction(async (tx) => {
await tx.insert(users).values(userData);
// エラーが発生した場合、自動的にロールバック
if (someCondition) {
throw new Error('Transaction failed');
}
});
} catch (error) {
console.error('Transaction rolled back:', error);
}
// プリペアドステートメント
const getUserById = db
.select()
.from(users)
.where(eq(users.id, placeholder('id')))
.prepare('getUserById');
// 実行
const user = await getUserById.execute({ id: 123 });
// バッチ処理
const batchInsert = async (items: any[]) => {
const BATCH_SIZE = 1000;
for (let i = 0; i < items.length; i += BATCH_SIZE) {
const batch = items.slice(i, i + BATCH_SIZE);
await db.insert(products).values(batch);
}
};
// 悪い例:N+1問題
const posts = await db.select().from(posts);
for (const post of posts) {
const author = await db
.select()
.from(users)
.where(eq(users.id, post.authorId));
}
// 良い例:JOINを使用
const postsWithAuthors = await db
.select()
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
// または、リレーションクエリを使用
const postsWithRelations = await db.query.posts.findMany({
with: {
author: true,
comments: {
with: {
author: true,
},
},
},
});
Drizzle は、エッジコンピューティング環境で輝きます。
プラットフォーム | 対応状況 | データベース | 特記事項 |
---|---|---|---|
Cloudflare Workers | ◎ | D1 | ネイティブサポート |
Vercel Edge | ◎ | Neon/PlanetScale | Serverless対応 |
Deno Deploy | ◎ | PostgreSQL | 完全対応 |
Bun | ◎ | 全DB | 高速実行 |
// worker.ts
import { Hono } from 'hono';
import { drizzle } from 'drizzle-orm/d1';
import { users, posts } from './schema';
type Env = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Env }>();
app.get('/api/users', async (c) => {
const db = drizzle(c.env.DB);
const allUsers = await db.select().from(users);
return c.json(allUsers);
});
app.post('/api/posts', async (c) => {
const db = drizzle(c.env.DB);
const body = await c.req.json();
const newPost = await db
.insert(posts)
.values(body)
.returning();
return c.json(newPost[0]);
});
export default app;
実際のアプリケーションでの使用例を紹介します。
// services/blog.service.ts
export class BlogService {
constructor(private db: Database) {}
async createPost(data: CreatePostDto, authorId: number) {
return await this.db.transaction(async (tx) => {
// 投稿を作成
const [post] = await tx
.insert(posts)
.values({
...data,
authorId,
slug: generateSlug(data.title),
})
.returning();
// タグを処理
if (data.tags?.length) {
const tagRecords = await this.findOrCreateTags(tx, data.tags);
await tx
.insert(postTags)
.values(
tagRecords.map(tag => ({
postId: post.id,
tagId: tag.id,
}))
);
}
return post;
});
}
async getPostsWithPagination(page = 1, limit = 10) {
const offset = (page - 1) * limit;
const posts = await this.db
.select({
id: posts.id,
title: posts.title,
slug: posts.slug,
excerpt: sql<string>`SUBSTRING(${posts.content}, 1, 200)`,
author: {
id: users.id,
username: users.username,
},
publishedAt: posts.publishedAt,
tags: sql<string[]>`
ARRAY_AGG(
DISTINCT ${tags.name}
) FILTER (WHERE ${tags.name} IS NOT NULL)
`,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.leftJoin(postTags, eq(posts.id, postTags.postId))
.leftJoin(tags, eq(postTags.tagId, tags.id))
.where(isNotNull(posts.publishedAt))
.groupBy(posts.id, users.id)
.orderBy(desc(posts.publishedAt))
.limit(limit)
.offset(offset);
const [{ total }] = await this.db
.select({ total: count() })
.from(posts)
.where(isNotNull(posts.publishedAt));
return {
posts,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
};
}
}
// PostgreSQLの全文検索
const searchPosts = async (query: string) => {
return await db
.select({
id: posts.id,
title: posts.title,
content: posts.content,
rank: sql<number>`
ts_rank(
to_tsvector('english', ${posts.title} || ' ' || ${posts.content}),
plainto_tsquery('english', ${query})
)
`,
})
.from(posts)
.where(
sql`
to_tsvector('english', ${posts.title} || ' ' || ${posts.content})
@@ plainto_tsquery('english', ${query})
`
)
.orderBy(desc(sql`rank`))
.limit(20);
};
多くの開発者が Prisma から Drizzle への移行を検討しています。
Prismaから移行した開発者の評価
Drizzle ORM は、typescript 時代の要求に応える革新的な ORM です。
✅ エッジコンピューティング: Cloudflare Workers、Vercel Edge 対応 ✅ 高パフォーマンス要求: 最小限のオーバーヘッド ✅ 型安全性重視: 完全な型推論 ✅ SQLの知識活用: sql ライクな api
⚠️ エコシステム: Prisma と比べてまだ発展途上 ⚠️ ドキュメント: 日本語情報が少ない ⚠️ マイグレーション: 複雑なマイグレーションには工夫が必要
Drizzle に移行してから、ビルドサイズが 80%削減され、 クエリのパフォーマンスも 30%向上しました。 何より、開発者体験が素晴らしいです。