1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Drizzle ORM × Claude Code:次世代のTypeScript開発体験

Last updated at Posted at 2025-09-04

🎯 この記事の概要

解決する問題

  • TypeScriptでの型安全なデータベース操作
  • ORMツールの選択に迷っている
  • AI支援開発との相性を知りたい

対象読者

  • TypeScript経験1年以上
  • データベース操作の基本知識
  • 効率的な開発ツールを探している方

前提知識

  • TypeScriptの基本文法
  • SQLの基本概念(SELECT、JOIN等)
  • Node.jsプロジェクトの構築経験

📊 結論・要点

Drizzle ORMをおすすめする理由

  • 完全な型安全性: コンパイル時にSQLエラーを検出
  • SQLライクな直感的記法: 学習コストが低い
  • AI開発との相性: 明示的なコードでClaude Codeが理解しやすい
  • 軽量設計: 最小限のオーバーヘッド

TypeScriptプロジェクトでデータベースを扱う際、Prisma、Supabase-js、TypeORMなど様々な選択肢があります。今回は、Drizzle ORMを使った開発体験と、AI支援開発ツールClaude Codeとの相性の良さについて、実際のプロジェクトでの経験を基に解説します。

💡 Drizzle ORMとは?

Drizzle ORMは、TypeScriptファーストで設計された軽量なORM(Object-Relational Mapping)ツールです。

主な特徴

  • SQLライクな記法: 既存のSQL知識を活かせる直感的なAPI
  • 完全な型安全性: TypeScriptの型システムを活用してコンパイル時エラー検出
  • 軽量設計: 最小限のランタイムオーバーヘッド
  • マルチデータベース対応: PostgreSQL、MySQL、SQLiteをサポート

ORMとは?
ORM(Object-Relational Mapping)は、データベースのテーブルとプログラムのオブジェクトを対応付ける技術です。SQLを直接書く代わりに、プログラミング言語の記法でデータベース操作を行えます。

📊 主要ORMの比較

基本的なクエリの書き方

// Drizzle - SQLに近い直感的な記法
const users = await db
  .select({
    id: users.id,
    name: users.name,
    postCount: count(posts.id)
  })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .where(eq(users.isActive, true))
  .groupBy(users.id);

// Prisma - 独自のオブジェクト記法
const users = await prisma.user.findMany({
  where: { isActive: true },
  include: {
    _count: {
      select: { posts: true }
    }
  }
});

// Supabase-js - チェーンメソッド
const { data } = await supabase
  .from('users')
  .select(`
    id,
    name,
    posts(count)
  `)
  .eq('is_active', true);

型安全性の比較

特徴 Drizzle Prisma Supabase-js TypeORM
コンパイル時型チェック ✅ 完全 ✅ 完全 ⚠️ 部分的 ⚠️ 部分的
スキーマからの型生成 ✅ TypeScript定義 ✅ 自動生成 ⚠️ 手動/生成 ✅ デコレータ
JOINの型推論 ✅ 自動 ✅ 自動 ❌ 手動 ⚠️ 部分的
SQLクエリの型安全性 ✅ ビルダー経由 ⚠️ Raw SQLは未対応 ❌ 文字列 ⚠️ 部分的
実行時の型検証 ❌ なし ✅ あり ❌ なし ⚠️ 部分的

🚀 Drizzle ORMの実装例

1. スキーマ定義

// schema/users.ts
import { pgTable, text, boolean, timestamp, uuid } from 'drizzle-orm/pg-core';

export const users = pgTable('users', {
  id: uuid('id').primaryKey().defaultRandom(),
  email: text('email').notNull().unique(),
  name: text('name').notNull(),
  isActive: boolean('is_active').default(true),
  createdAt: timestamp('created_at').notNull().defaultNow(),
  updatedAt: timestamp('updated_at').notNull().defaultNow(),
});

export const posts = pgTable('posts', {
  id: uuid('id').primaryKey().defaultRandom(),
  userId: uuid('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  createdAt: timestamp('created_at').notNull().defaultNow(),
});

2. 複雑なクエリの実装

// 投稿数とともにアクティブユーザーを取得
async function getActiveUsersWithStats() {
  const result = await db
    .select({
      userId: users.id,
      userName: users.name,
      email: users.email,
      totalPosts: count(posts.id),
      publishedPosts: count(
        case_().when(posts.published, 1).else(null)
      ),
      latestPostDate: max(posts.createdAt),
    })
    .from(users)
    .leftJoin(posts, eq(users.id, posts.userId))
    .where(eq(users.isActive, true))
    .groupBy(users.id)
    .having(gt(count(posts.id), 0))
    .orderBy(desc(count(posts.id)));

  return result;
}

3. トランザクション処理

// ユーザーと初期投稿を同時に作成
async function createUserWithWelcomePost(userData: NewUser) {
  return await db.transaction(async (tx) => {
    // ユーザー作成
    const [newUser] = await tx
      .insert(users)
      .values(userData)
      .returning();

    // ウェルカム投稿を作成
    const [welcomePost] = await tx
      .insert(posts)
      .values({
        userId: newUser.id,
        title: 'Welcome to our platform!',
        content: `Hello ${newUser.name}, welcome aboard!`,
        published: true,
      })
      .returning();

    return { user: newUser, post: welcomePost };
  });
}

🤖 Claude CodeとDrizzleの相性が良い理由

1. 明示的なコード生成

Claude Codeは、SQLの知識を直接活用してDrizzleのクエリを生成できます:

// Claude Codeへの指示例
"ユーザーの最新10件の投稿を取得するクエリを書いて"

// 生成されるコード
const recentPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.userId, userId))
  .orderBy(desc(posts.createdAt))
  .limit(10);

2. 段階的な実装サポート

// Step 1: 基本クエリから開始
const allUsers = await db.select().from(users);

// Step 2: 条件を追加
const activeUsers = await db
  .select()
  .from(users)
  .where(eq(users.isActive, true));

// Step 3: JOINを追加
const usersWithPosts = await db
  .select()
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .where(eq(users.isActive, true));

// Step 4: 集計を追加
const userStats = await db
  .select({
    user: users,
    postCount: count(posts.id)
  })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.userId))
  .groupBy(users.id);

3. エラーの明確性

// TypeScriptの型エラーが具体的
db.select()
  .from(users)
  .where(eq(users.email, 123)); // ❌ Type error: number is not assignable to string

// SQLエラーも理解しやすい
db.select()
  .from(users)
  .where(eq(users.nonExistentColumn, 'value')); // ❌ Property 'nonExistentColumn' does not exist

💡 Drizzleが特に優れているユースケース

1. 複雑なJOINが必要な場合

// 複数テーブルを結合した統計情報の取得
const analytics = await db
  .select({
    date: sql<string>`DATE(${orders.createdAt})`,
    totalOrders: count(orders.id),
    uniqueCustomers: countDistinct(orders.customerId),
    totalRevenue: sum(orderItems.price),
    avgOrderValue: avg(orderItems.price),
  })
  .from(orders)
  .leftJoin(orderItems, eq(orders.id, orderItems.orderId))
  .leftJoin(customers, eq(orders.customerId, customers.id))
  .where(gte(orders.createdAt, lastMonth))
  .groupBy(sql`DATE(${orders.createdAt})`);

2. 動的クエリの構築

function buildDynamicQuery(filters: FilterOptions) {
  let query = db.select().from(products);
  
  const conditions = [];
  
  if (filters.category) {
    conditions.push(eq(products.category, filters.category));
  }
  
  if (filters.minPrice) {
    conditions.push(gte(products.price, filters.minPrice));
  }
  
  if (filters.inStock) {
    conditions.push(gt(products.stock, 0));
  }
  
  if (conditions.length > 0) {
    query = query.where(and(...conditions));
  }
  
  if (filters.sortBy) {
    query = query.orderBy(
      filters.sortOrder === 'desc' 
        ? desc(products[filters.sortBy])
        : asc(products[filters.sortBy])
    );
  }
  
  return query;
}

3. 生SQLが必要な場合

// Window関数を使った高度なクエリ
const rankedProducts = await db.execute(sql`
  WITH RankedProducts AS (
    SELECT 
      *,
      ROW_NUMBER() OVER (PARTITION BY category ORDER BY sales DESC) as rank
    FROM products
  )
  SELECT * FROM RankedProducts WHERE rank <= 5
`);

🎯 導入のベストプラクティス

1. プロジェクトのセットアップ

# 必要なパッケージのインストール
npm install drizzle-orm postgres
npm install -D drizzle-kit @types/pg

# 設定ファイルの作成
touch drizzle.config.ts
// drizzle.config.ts
import type { Config } from 'drizzle-kit';

export default {
  schema: './src/db/schema/*',
  out: './drizzle',
  driver: 'pg',
  dbCredentials: {
    connectionString: process.env.DATABASE_URL!,
  },
} satisfies Config;

2. 接続設定

// src/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 sql = postgres(connectionString);

export const db = drizzle(sql, { schema });

3. マイグレーション

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

# マイグレーションの実行
npx drizzle-kit push:pg

🚀 まとめ

Drizzle ORMは、以下の特徴により、特にClaude CodeのようなAI支援ツールとの相性が抜群です:

SQLライクな直感的な記法

  • SQLの知識をそのまま活用できる
  • 生成されるクエリが予測可能

完全な型安全性

  • コンパイル時にエラーを検出
  • IDEの補完機能を最大限活用

最小限のオーバーヘッド

  • 薄いラッパーレイヤー
  • 高速な実行速度

柔軟性

  • 複雑なクエリも型安全に記述
  • 生SQLへのエスケープハッチ

次のステップ

  1. Drizzle ORM公式ドキュメントで基本概念を学習
  2. 小規模なプロジェクトで実際に試してみる
  3. Claude Codeと組み合わせた開発フローを体験

特に、複雑なJOINや集計処理が必要なプロジェクトでは、Drizzle ORMの採用により、開発効率の大きな改善が期待できます。

AI支援開発が当たり前になりつつある現在、明示的で予測可能なコードを生成できるDrizzle ORMは、次世代のTypeScript開発における有力な選択肢となるでしょう。

📚 参考資料

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?