ブログ記事

Drizzle ORM完全ガイド - TypeScript時代の新世代データベースツール

Drizzle ORMは、TypeScriptファーストで設計された軽量かつ型安全なORMです。Prismaとの比較を交えながら、マイグレーション、スキーマ定義、クエリビルダーなど、実践的な使い方を詳しく解説します。

Web開発
Drizzle ORM TypeScript データベース SQL
Drizzle ORM完全ガイド - TypeScript時代の新世代データベースツールのヒーロー画像

Typescript プロジェクトでデータベースを扱う際、型安全性とパフォーマンスの両立に悩んでいませんか? Drizzle ORM は、軽量でありながら強力な型推論を提供する新世代の ORM です。 本記事では、Prisma との比較を含め、実践的な Drizzle ORM の使い方を徹底解説します。

この記事で学べること

  • Drizzle ORM の設計思想と他 ORM との違い
  • 型安全なスキーマ定義とリレーション
  • 高度なクエリビルダーの使い方
  • マイグレーション戦略とベストプラクティス
  • エッジランタイムでの活用方法

目次

  1. なぜ Drizzle ORM なのか?typescript ネイティブの利点
  2. 基本概念 - Drizzle の設計思想
  3. 環境構築とデータベース接続
  4. スキーマ定義 - 型安全なテーブル設計
  5. クエリビルダー - sql ライクな api
  6. マイグレーション - Drizzle Kit 活用
  7. 高度な機能 - トランザクションと最適化
  8. エッジランタイム対応
  9. 実践的な実装例
  10. Prisma との比較と移行ガイド

なぜDrizzle ORMなのか?TypeScriptネイティブの利点

2025 年、typescript は標準となり、型安全性は開発の必須要件となりました。 Drizzle ORM は、この要求に応える次世代の ORM として設計されています。

ORMの進化

チャートを読み込み中...

Drizzleの革新的な特徴

  • 真の型安全性: スキーマからクエリまで完全な型推論
  • 軽量設計: Prisma の 1/10 のバンドルサイズ
  • エッジ対応: Cloudflare Workers、Vercel Edge Runtime 対応
  • SQLライク: 学習コストが低く、sql の知識を活かせる
主要ORMの比較
特徴 Drizzle Prisma TypeORM
バンドルサイズ 50KB 500KB+ 300KB+
型安全性 完全 高い 中程度
エッジ対応 ×
パフォーマンス 高速 標準 標準
学習曲線 緩やか 中程度

基本概念 - Drizzleの設計思想

Drizzle は「sql を隠さない」設計思想を持っています。 sql の知識を活かしながら、型安全性を享受できます。

// schema.prisma model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] } // 使用時 const users = await prisma.user.findMany({ where: { email: { contains: '@example.com' } }, include: { posts: true } });
// schema.ts export const users = pgTable('users', { id: serial('id').primaryKey(), email: text('email').unique().notNull(), name: text('name') }); // 使用時 const result = await db .select() .from(users) .where(like(users.email, '%@example.com%')) .leftJoin(posts, eq(posts.userId, users.id));
Prisma アプローチ
// schema.prisma model User { id Int @id @default(autoincrement()) email String @unique name String? posts Post[] } // 使用時 const users = await prisma.user.findMany({ where: { email: { contains: '@example.com' } }, include: { posts: true } });
Drizzle アプローチ
// schema.ts export const users = pgTable('users', { id: serial('id').primaryKey(), email: text('email').unique().notNull(), name: text('name') }); // 使用時 const result = await db .select() .from(users) .where(like(users.email, '%@example.com%')) .leftJoin(posts, eq(posts.userId, users.id));

設計原則

  1. 透明性: sql クエリがどのように生成されるか明確
  2. 型推論: 明示的な型定義なしで完全な型安全性
  3. パフォーマンス: 最小限のオーバーヘッド
  4. 柔軟性: 生 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],
  }),
}));

クエリビルダー - SQLライクなapi

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、UPDATE、DELETE

// 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活用

Drizzle Kit は、スキーマの変更を管理する強力なツールです。

マイグレーションの生成

# マイグレーションファイルを生成
bun drizzle-kit generate:pg

# 生成されたマイグレーションを確認
bun drizzle-kit migrate

# SQLを直接実行せずに確認
bun drizzle-kit push:pg --dry-run

マイグレーション戦略

drizzle-kit push

スキーマを直接同期

generate & migrate

マイグレーションファイル生成

migrate with review

レビュー後に適用

down migration

必要に応じて戻す

カスタムマイグレーション

// マイグレーション実行
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問題の回避

// 悪い例: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 高速実行

Cloudflare Workers実装例

// 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との比較と移行ガイド

多くの開発者が Prisma から Drizzle への移行を検討しています。

// schema.prisma model User { id Int @id @default(autoincrement()) email String @unique posts Post[] profile Profile? } // 使用 const user = await prisma.user.create({ data: { email: 'test@example.com', posts: { create: [ { title: 'Post 1' }, { title: 'Post 2' } ] } }, include: { posts: true } });
// schema.ts export const users = pgTable('users', { id: serial('id').primaryKey(), email: text('email').unique().notNull(), }); // 使用 const user = await db.transaction(async (tx) => { const [newUser] = await tx .insert(users) .values({ email: 'test@example.com' }) .returning(); await tx .insert(posts) .values([ { title: 'Post 1', authorId: newUser.id }, { title: 'Post 2', authorId: newUser.id } ]); return newUser; });
Prisma
// schema.prisma model User { id Int @id @default(autoincrement()) email String @unique posts Post[] profile Profile? } // 使用 const user = await prisma.user.create({ data: { email: 'test@example.com', posts: { create: [ { title: 'Post 1' }, { title: 'Post 2' } ] } }, include: { posts: true } });
Drizzle
// schema.ts export const users = pgTable('users', { id: serial('id').primaryKey(), email: text('email').unique().notNull(), }); // 使用 const user = await db.transaction(async (tx) => { const [newUser] = await tx .insert(users) .values({ email: 'test@example.com' }) .returning(); await tx .insert(posts) .values([ { title: 'Post 1', authorId: newUser.id }, { title: 'Post 2', authorId: newUser.id } ]); return newUser; });

移行チェックリスト

  • スキーマを Drizzle 形式に変換
  • マイグレーション履歴の移行
  • クエリの書き換え
  • リレーションクエリの調整
  • ミドルウェアの更新
開発者満足度 85 %
パフォーマンス向上 95 %
学習コスト削減 70 %

Prismaから移行した開発者の評価

まとめ

Drizzle ORM は、typescript 時代の要求に応える革新的な ORM です。

採用すべきケース

エッジコンピューティング: Cloudflare Workers、Vercel Edge 対応 ✅ 高パフォーマンス要求: 最小限のオーバーヘッド ✅ 型安全性重視: 完全な型推論 ✅ SQLの知識活用: sql ライクな api

注意点

⚠️ エコシステム: Prisma と比べてまだ発展途上 ⚠️ ドキュメント: 日本語情報が少ない ⚠️ マイグレーション: 複雑なマイグレーションには工夫が必要

Drizzle に移行してから、ビルドサイズが 80%削減され、 クエリのパフォーマンスも 30%向上しました。 何より、開発者体験が素晴らしいです。

開発チーム Drizzle採用企業

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。