Drizzle
DrizzleはTypeScript向けの軽量でサーバーレス対応の次世代ORMライブラリです。「SQLを知っていればDrizzleを知っている」をモットーに、SQLライクな設計とゼロ依存関係による最小限の抽象化を提供します。7.4KB(minified+gzipped)という軽量性とコールドスタート最適化により、サーバーレス環境で真価を発揮し、Node.js、Bun、Denoで動作する現代的なTypeScript開発の理想的なデータベースソリューションです。
GitHub概要
drizzle-team/drizzle-orm
Headless TypeScript ORM with a head. Runs on Node, Bun and Deno. Lives on the Edge and yes, it's a JavaScript ORM too 😅
トピックス
スター履歴
ライブラリ
Drizzle
概要
DrizzleはTypeScript向けの軽量でサーバーレス対応の次世代ORMライブラリです。「SQLを知っていればDrizzleを知っている」をモットーに、SQLライクな設計とゼロ依存関係による最小限の抽象化を提供します。7.4KB(minified+gzipped)という軽量性とコールドスタート最適化により、サーバーレス環境で真価を発揮し、Node.js、Bun、Denoで動作する現代的なTypeScript開発の理想的なデータベースソリューションです。
詳細
Drizzle 2025年版は、TypeScriptエコシステムにおいて「軽量性とパフォーマンス」を重視する開発者の第一選択として確立されています。PostgreSQL、MySQL、SQLiteを含むあらゆるデータベース(Turso、Neon、Xata等のサーバーレスデータベース含む)をサポートし、常に1つのSQLクエリを出力する効率的な設計を採用。SQLファーストアプローチにより学習コストを最小化しつつ、強力な型安全性とIDE支援を提供します。バンドルサイズとメモリフットプリントの最小化により、リソース制約のあるサーバーレスプラットフォームでの優位性を発揮します。
主な特徴
- ゼロ依存関係: 外部依存なしで7.4KBの超軽量設計
- サーバーレス最適化: コールドスタート時間とメモリ使用量を最小化
- SQLファーストアプローチ: SQLの全機能を活用できる最小限の抽象化
- 完全な型安全性: TypeScriptファーストの設計による強力な型推論
- マルチプラットフォーム: Node.js、Bun、Denoでの動作をサポート
- 豊富なデータベース対応: PostgreSQL、MySQL、SQLite完全対応
メリット・デメリット
メリット
- 軽量性によるサーバーレス環境での優れたパフォーマンス
- SQLの知識をそのまま活用できる学習コストの低さ
- ゼロ依存関係による高いセキュリティとメンテナンス性
- 常に最適化された単一SQLクエリによる高効率
- 強力な型安全性とIDE支援による開発効率向上
- サーバーレスデータベースとの優れた統合性
デメリット
- 高度なORM機能(自動マイグレーション等)の不足
- SQLの知識が必須で、SQL初心者には学習コストが高い
- 複雑なリレーション操作では手動でのクエリ記述が必要
- コミュニティとエコシステムが他の大手ORMより小規模
- エンタープライズ向けの高度な機能が限定的
- Prismaのような包括的な開発者体験ツールが不足
参考ページ
書き方の例
基本セットアップ
// package.json
npm install drizzle-orm
npm install -D drizzle-kit
// PostgreSQLの場合
npm install pg
npm install -D @types/pg
// MySQLの場合
npm install mysql2
// SQLiteの場合
npm install better-sqlite3
npm install -D @types/better-sqlite3
// ドライバー例(Node.js)
npm install postgres // postgres.js
npm install @neondatabase/serverless // Neon
npm install @planetscale/database // PlanetScale
モデル定義と基本操作
// schema.ts
import { pgTable, serial, varchar, integer, timestamp, text } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';
// テーブル定義
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull(),
email: varchar('email', { length: 255 }).unique().notNull(),
age: integer('age'),
createdAt: timestamp('created_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: varchar('title', { length: 200 }).notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
});
// リレーション定義
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
}));
// 型推論
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
// db.ts - データベース接続設定
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
// PostgreSQLクライアント設定
const client = postgres(process.env.DATABASE_URL!);
export const db = drizzle(client, { schema });
// 基本的なCRUD操作
export class UserRepository {
// Create
static async createUser(userData: NewUser): Promise<User> {
const [user] = await db.insert(users).values(userData).returning();
return user;
}
// Read
static async getAllUsers(): Promise<User[]> {
return await db.select().from(users);
}
static async getUserById(id: number): Promise<User | undefined> {
const [user] = await db
.select()
.from(users)
.where(eq(users.id, id));
return user;
}
// Update
static async updateUser(id: number, userData: Partial<NewUser>): Promise<User> {
const [user] = await db
.update(users)
.set(userData)
.where(eq(users.id, id))
.returning();
return user;
}
// Delete
static async deleteUser(id: number): Promise<void> {
await db.delete(users).where(eq(users.id, id));
}
}
// 使用例
async function main() {
// ユーザー作成
const newUser = await UserRepository.createUser({
name: '田中太郎',
email: '[email protected]',
age: 30,
});
console.log('Created user:', newUser);
// 全ユーザー取得
const allUsers = await UserRepository.getAllUsers();
console.log('All users:', allUsers);
// ユーザー更新
const updatedUser = await UserRepository.updateUser(newUser.id, {
age: 31,
});
console.log('Updated user:', updatedUser);
}
高度なクエリ操作
import { eq, and, or, like, gt, gte, lt, lte, inArray, isNull, isNotNull } from 'drizzle-orm';
import { count, sum, avg, max, min } from 'drizzle-orm';
export class AdvancedQueries {
// 複雑な条件でのフィルタリング
static async getUsersByConditions(
minAge?: number,
maxAge?: number,
namePattern?: string
): Promise<User[]> {
const conditions = [];
if (minAge !== undefined) conditions.push(gte(users.age, minAge));
if (maxAge !== undefined) conditions.push(lte(users.age, maxAge));
if (namePattern) conditions.push(like(users.name, `%${namePattern}%`));
return await db
.select()
.from(users)
.where(and(...conditions))
.orderBy(users.name);
}
// JOIN操作
static async getUsersWithPosts(): Promise<Array<User & { posts: Post[] }>> {
return await db.query.users.findMany({
with: {
posts: true,
},
});
}
// 集約クエリ
static async getUserStatistics() {
const [stats] = await db
.select({
totalUsers: count(users.id),
averageAge: avg(users.age),
oldestUser: max(users.age),
youngestUser: min(users.age),
})
.from(users);
return stats;
}
// サブクエリとCTE(Common Table Expression)
static async getActiveUsersWithRecentPosts() {
const activeUsers = db
.select({ userId: posts.authorId })
.from(posts)
.where(gte(posts.createdAt, new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)))
.groupBy(posts.authorId)
.having(gt(count(posts.id), 5));
return await db
.select()
.from(users)
.where(inArray(users.id, activeUsers));
}
// カスタムSQL
static async getTopAuthors(limit: number = 10) {
return await db.execute(sql`
SELECT
u.id,
u.name,
u.email,
COUNT(p.id) as post_count
FROM ${users} u
LEFT JOIN ${posts} p ON u.id = p.author_id
GROUP BY u.id, u.name, u.email
ORDER BY post_count DESC
LIMIT ${limit}
`);
}
// トランザクション
static async transferPosts(fromUserId: number, toUserId: number) {
return await db.transaction(async (tx) => {
// 転送元ユーザーの存在確認
const fromUser = await tx
.select()
.from(users)
.where(eq(users.id, fromUserId));
if (fromUser.length === 0) {
throw new Error('Transfer source user not found');
}
// 転送先ユーザーの存在確認
const toUser = await tx
.select()
.from(users)
.where(eq(users.id, toUserId));
if (toUser.length === 0) {
throw new Error('Transfer destination user not found');
}
// 投稿の移転
const result = await tx
.update(posts)
.set({ authorId: toUserId })
.where(eq(posts.authorId, fromUserId))
.returning();
return result;
});
}
// バッチ操作
static async batchInsertUsers(usersData: NewUser[]): Promise<User[]> {
return await db.insert(users).values(usersData).returning();
}
static async batchUpdateUsers(updates: Array<{ id: number; data: Partial<NewUser> }>) {
return await db.transaction(async (tx) => {
const results = [];
for (const update of updates) {
const [result] = await tx
.update(users)
.set(update.data)
.where(eq(users.id, update.id))
.returning();
results.push(result);
}
return results;
});
}
}
// 使用例
async function advancedExamples() {
// 条件付き検索
const youngAdults = await AdvancedQueries.getUsersByConditions(18, 30, '田中');
console.log('Young adults:', youngAdults);
// 統計情報
const stats = await AdvancedQueries.getUserStatistics();
console.log('User statistics:', stats);
// バッチ挿入
const newUsers = await AdvancedQueries.batchInsertUsers([
{ name: 'ユーザー1', email: '[email protected]', age: 25 },
{ name: 'ユーザー2', email: '[email protected]', age: 30 },
{ name: 'ユーザー3', email: '[email protected]', age: 35 },
]);
console.log('Batch inserted users:', newUsers);
}
リレーション操作
// より複雑なリレーション
export const categories = pgTable('categories', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull(),
description: text('description'),
});
export const postCategories = pgTable('post_categories', {
postId: integer('post_id').references(() => posts.id),
categoryId: integer('category_id').references(() => categories.id),
}, (table) => ({
pk: primaryKey({ columns: [table.postId, table.categoryId] }),
}));
// リレーション定義の拡張
export const categoriesRelations = relations(categories, ({ many }) => ({
postCategories: many(postCategories),
}));
export const postCategoriesRelations = relations(postCategories, ({ one }) => ({
post: one(posts, { fields: [postCategories.postId], references: [posts.id] }),
category: one(categories, { fields: [postCategories.categoryId], references: [categories.id] }),
}));
export const extendedPostsRelations = relations(posts, ({ one, many }) => ({
author: one(users, { fields: [posts.authorId], references: [users.id] }),
postCategories: many(postCategories),
}));
export class RelationQueries {
// ネストしたリレーションクエリ
static async getPostsWithAuthorsAndCategories() {
return await db.query.posts.findMany({
with: {
author: true,
postCategories: {
with: {
category: true,
},
},
},
});
}
// 特定カテゴリの投稿を取得
static async getPostsByCategory(categoryName: string) {
return await db
.select({
id: posts.id,
title: posts.title,
content: posts.content,
authorName: users.name,
categoryName: categories.name,
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.innerJoin(postCategories, eq(posts.id, postCategories.postId))
.innerJoin(categories, eq(postCategories.categoryId, categories.id))
.where(eq(categories.name, categoryName));
}
// ユーザーの投稿をカテゴリ別に集計
static async getUserPostsByCategory(userId: number) {
return await db
.select({
categoryName: categories.name,
postCount: count(posts.id),
})
.from(posts)
.innerJoin(postCategories, eq(posts.id, postCategories.postId))
.innerJoin(categories, eq(postCategories.categoryId, categories.id))
.where(eq(posts.authorId, userId))
.groupBy(categories.id, categories.name);
}
// 多対多リレーションの管理
static async addPostToCategories(postId: number, categoryIds: number[]) {
const values = categoryIds.map(categoryId => ({
postId,
categoryId,
}));
return await db.insert(postCategories).values(values);
}
static async removePostFromCategories(postId: number, categoryIds?: number[]) {
let query = db.delete(postCategories).where(eq(postCategories.postId, postId));
if (categoryIds) {
query = query.where(inArray(postCategories.categoryId, categoryIds));
}
return await query;
}
// 複雑なJOIN操作
static async getPopularCategories(limit: number = 10) {
return await db
.select({
categoryId: categories.id,
categoryName: categories.name,
postCount: count(posts.id),
uniqueAuthors: sql<number>`COUNT(DISTINCT ${posts.authorId})`.as('unique_authors'),
})
.from(categories)
.leftJoin(postCategories, eq(categories.id, postCategories.categoryId))
.leftJoin(posts, eq(postCategories.postId, posts.id))
.groupBy(categories.id, categories.name)
.orderBy(desc(count(posts.id)))
.limit(limit);
}
}
実用例
// Express.jsアプリケーションでの実用例
import express from 'express';
import { z } from 'zod';
const app = express();
app.use(express.json());
// バリデーションスキーマ
const createUserSchema = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
});
const updateUserSchema = createUserSchema.partial();
// APIエンドポイント
app.get('/users', async (req, res) => {
try {
const { page = 1, limit = 10, search, minAge, maxAge } = req.query;
let query = db.select().from(users);
const conditions = [];
if (search) {
conditions.push(like(users.name, `%${search}%`));
}
if (minAge) {
conditions.push(gte(users.age, Number(minAge)));
}
if (maxAge) {
conditions.push(lte(users.age, Number(maxAge)));
}
if (conditions.length > 0) {
query = query.where(and(...conditions));
}
const users = await query
.limit(Number(limit))
.offset((Number(page) - 1) * Number(limit))
.orderBy(users.createdAt);
res.json({
data: users,
pagination: {
page: Number(page),
limit: Number(limit),
},
});
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.get('/users/:id', async (req, res) => {
try {
const userId = Number(req.params.id);
const user = await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
posts: {
with: {
postCategories: {
with: {
category: true,
},
},
},
},
},
});
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
app.post('/users', async (req, res) => {
try {
const userData = createUserSchema.parse(req.body);
const [newUser] = await db.insert(users).values(userData).returning();
res.status(201).json(newUser);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Validation error', details: error.errors });
}
res.status(500).json({ error: 'Internal server error' });
}
});
app.put('/users/:id', async (req, res) => {
try {
const userId = Number(req.params.id);
const userData = updateUserSchema.parse(req.body);
const [updatedUser] = await db
.update(users)
.set(userData)
.where(eq(users.id, userId))
.returning();
if (!updatedUser) {
return res.status(404).json({ error: 'User not found' });
}
res.json(updatedUser);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Validation error', details: error.errors });
}
res.status(500).json({ error: 'Internal server error' });
}
});
app.delete('/users/:id', async (req, res) => {
try {
const userId = Number(req.params.id);
const result = await db.delete(users).where(eq(users.id, userId)).returning();
if (result.length === 0) {
return res.status(404).json({ error: 'User not found' });
}
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
});
// サーバーレス関数での使用例(Vercel)
export default async function handler(req: any, res: any) {
if (req.method === 'GET') {
try {
const users = await db.select().from(users).limit(10);
res.status(200).json(users);
} catch (error) {
res.status(500).json({ error: 'Database error' });
}
} else {
res.setHeader('Allow', ['GET']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
// Next.js API Routeでの使用例
// pages/api/users/[id].ts
export default async function handler(req: any, res: any) {
const { id } = req.query;
const userId = Number(id);
switch (req.method) {
case 'GET':
try {
const user = await UserRepository.getUserById(userId);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
break;
case 'PUT':
try {
const userData = updateUserSchema.parse(req.body);
const updatedUser = await UserRepository.updateUser(userId, userData);
res.json(updatedUser);
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Validation error', details: error.errors });
}
res.status(500).json({ error: 'Internal server error' });
}
break;
case 'DELETE':
try {
await UserRepository.deleteUser(userId);
res.status(204).end();
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
break;
default:
res.setHeader('Allow', ['GET', 'PUT', 'DELETE']);
res.status(405).end(`Method ${req.method} Not Allowed`);
}
}
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});