Drizzle

DrizzleはTypeScript向けの軽量でサーバーレス対応の次世代ORMライブラリです。「SQLを知っていればDrizzleを知っている」をモットーに、SQLライクな設計とゼロ依存関係による最小限の抽象化を提供します。7.4KB(minified+gzipped)という軽量性とコールドスタート最適化により、サーバーレス環境で真価を発揮し、Node.js、Bun、Denoで動作する現代的なTypeScript開発の理想的なデータベースソリューションです。

ORMTypeScript軽量サーバーレスSQLゼロ依存タイプセーフ

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 😅

スター29,260
ウォッチ55
フォーク936
作成日:2021年6月24日
言語:TypeScript
ライセンス:Apache License 2.0

トピックス

bunjsd1libsqllitefsmysqlmysql2neonnodejsormpostgrespostgresjspostgresqlsqlsqlitesqlite3sqljstursotypescriptvercel-postgres

スター履歴

drizzle-team/drizzle-orm Star History
データ取得日時: 2025/7/17 10:32

ライブラリ

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}`);
});