TypeScriptとPostgreSQLで始めるDrizzle ORM入門:型安全・超軽量・高パフォーマンスなデータベース操作

Database tutorial - IT technology blog
Database tutorial - IT technology blog

TypeScriptにおけるデータベース操作手法の比較

PostgreSQLとTypeScriptでバックエンドを構築する際、主に以下の3つのアプローチで迷うことが多いでしょう。

  • 生SQL / クエリビルダー(pg, Knex.js):生SQLを記述することで最大限のパフォーマンスを引き出せます。データベースに送信されるクエリを100%制御できる反面、TypeScriptの型マッピングを手動で行う必要があり、抜け漏れが発生しやすくなります。カラム名のわずかなタイプミス一つで、本番環境のAPIがダウンするリスクを伴います。
  • 重量級ORM(TypeORM, Prisma):Prismaは型の自動生成などにより優れたDX(開発者体験)を提供します。しかし、15〜30MBにおよぶRust製クエリエンジンが大きなデメリットとなります。AWS LambdaやVercel Serverlessなどの環境では、このエンジンが原因でコールドスタートが300ms〜1秒にまで膨らむことがあり、予期せぬ複雑なネストクエリが生成されるケースもあります。
  • 軽量・型安全ORM(Drizzle ORM):「SQLを知っていれば、Drizzleを理解できる(If you know SQL, you know Drizzle)」という哲学に基づいて設計されています。重厚なエンジンやランタイムマジックは一切ありません。わずか~30KBの軽量サイズで、TypeScriptの構文をSQLへ1対1で直接変換し、スキーマ定義からクエリ結果に至るまで完全な型安全性を維持します。

Drizzle ORMの実践におけるメリット・デメリット

最近、2,000 req/sを超えるトラフィックを処理するプロジェクトにおいて、PostgreSQL(RDS db.t4g.medium)上のORMをPrismaからDrizzle ORMへと移行しました。数ヶ月間の本番運用を経て見えてきたリアルな知見を共有します。

主なメリット

  • 瞬時の起動(ゼロ・エンジンオーバーヘッド):極めて軽量なバンドルサイズにより、ServerlessやCloudflare Workersにおけるコールドスタートを15ms未満に抑えられます。
  • 生SQLに近い直感的な構文:多数の独自抽象化メソッドを覚える必要はありません。標準的なSQLの知識があれば、すぐにDrizzleでコードを書くことができます。
  • 高精度な自動型推論:クエリ結果の型はスキーマから自動的に推論されます。データベース定義を変更するたびに、ビルドコマンドや中間コードの生成処理を実行する必要がありません。
  • 便利なDrizzle Kitツールチェーン:スキーマの差分を自動検出してマイグレーションファイルを生成できるほか、DBeaverやpgAdminをインストールすることなく、ブラウザ上で直感的にデータを閲覧・操作できるDrizzle Studioが標準で用意されています。

検討すべき注意点

  • PrismaやTypeORMと比較すると、エコシステムやStackOverflowなどのコミュニティ情報はまだ発展途上です。稀なエラーに遭遇した際は、GitHub上のソースコードを直接確認する必要がある場合があります。
  • 複雑な多対多(N-N)のリレーションを扱う場合、ORMに完全に頼るのではなく、適切なJOINを組み立てるための確かなSQLの設計力が求められます。

どのようなケースでDrizzle ORMを採用すべきか?

Drizzleは、以下のようなシナリオでその真価を最大限に発揮します。

  • 起動時間の最小化が求められるEdge Functions、Cloudflare Workers、AWS Lambda上で動作するアプリケーション。
  • 明示的なSQLの記述を好み、データベースに発行されるクエリを完全にコントロールしたい開発チーム。
  • サーバーのRAMやCPUリソースを最適化しつつ、TypeScriptの堅牢な型安全性を確保したいプロジェクト。

PostgreSQL環境におけるDrizzle ORMの導入手順

ステップ1: プロジェクトの初期化とパッケージのインストール

まず、プロジェクトを初期化し、Drizzle本体とドライバーであるpostgres(postgres.js)をインストールします。

mkdir drizzle-pg-demo && cd drizzle-pg-demo
npm init -y
npm install drizzle-orm postgres dotenv
npm install -D typescript @types/node drizzle-kit tsx
npx tsc --init

ステップ2: データベース接続の設定

接続文字列を管理するため、.envファイルを作成します。

DATABASE_URL="postgres://postgres:password@localhost:5432/my_db"

続いて、src/db/index.tsにクライアントの初期化処理を実装します。

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
import 'dotenv/config';

const connectionString = process.env.DATABASE_URL!;

// コネクションプールを指定してpostgres-jsクライアントを初期化
const client = postgres(connectionString, { max: 10 });
export const db = drizzle(client, { schema });

ステップ3: 型安全なスキーマ定義

src/db/schema.tsを作成します。ここでは、1対多のリレーションを持つusersテーブルとpostsテーブルを定義します。

import { pgTable, serial, text, timestamp, integer } from 'drizzle-orm/pg-core';
import { relations } from 'drizzle-orm';

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  fullName: text('full_name').notNull(),
  email: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: text('title').notNull(),
  content: text('content'),
  authorId: integer('author_id')
    .references(() => users.id, { onDelete: 'cascade' })
    .notNull(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
});

// ネストクエリを実行するためのリレーション定義
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts),
}));

export const postsRelations = relations(posts, ({ one }) => ({
  author: one(users, {
    fields: [posts.authorId],
    references: [users.id],
  }),
}));

// スキーマからTypeScriptの型を直接抽出
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;

ステップ4: Drizzle Kitの設定とスキーマの同期

ルートディレクトリに設定ファイルdrizzle.config.tsを作成します。

import { defineConfig } from 'drizzle-kit';
import 'dotenv/config';

export default defineConfig({
  schema: './src/db/schema.ts',
  out: './drizzle',
  dialect: 'postgresql',
  dbCredentials: {
    url: process.env.DATABASE_URL!,
  },
});

ローカル環境のデータベースにスキーマを直接反映します。

# スキーマをPostgreSQLに直接同期(ローカル開発向け)
npx drizzle-kit push

# ポート4983でビジュアルGUI管理ツールを起動
npx drizzle-kit studio

注意:本番環境では、変更履歴を厳密に管理するため、npx drizzle-kit generateでSQLマイグレーションファイルを生成し、CI/CDパイプライン経由でmigrate()を実行してください。

ステップ5: 実践的なCRUD操作の実装

src/index.tsを作成し、挿入、取得、およびリレーショナルクエリの動作を確認します。

import { db } from './db';
import { users, posts } from './db/schema';
import { eq, desc } from 'drizzle-orm';

async function main() {
  // 1. 新規ユーザーを挿入し、作成されたレコードを即座に取得
  const [newUser] = await db.insert(users).values({
    fullName: 'Nguyen Van A',
    email: `vana_${Date.now()}@example.com`,
  }).returning();
  console.log('Inserted User:', newUser);

  // 2. ユーザーに紐づく記事(Post)を挿入
  await db.insert(posts).values({
    title: '10分で学ぶDrizzle ORM',
    content: 'Drizzle ORMは生SQLのような極めてスムーズな操作感を提供します。',
    authorId: newUser.id,
  });

  // 3. 標準的なSQL構文によるクエリ取得
  const allUsers = await db.select().from(users).orderBy(desc(users.createdAt)).limit(5);
  console.log('Top 5 Users:', allUsers);

  // 4. リレーショナルクエリ(ユーザーと紐づく全記事の一括取得)
  const usersWithPosts = await db.query.users.findMany({
    where: eq(users.id, newUser.id),
    with: {
      posts: true,
    },
  });
  console.log('User with posts:', JSON.stringify(usersWithPosts, null, 2));
}

main().catch(console.error);

スクリプトを実行して動作確認を行います。

npx tsx src/index.ts

本番環境へDrizzleを導入する際の3つの実践的Tips

  • コネクションプールの適切な管理:サーバーレス環境にデプロイする場合、各インスタンスが個別にプールを確立するため、PostgreSQLの接続上限に達する恐れがあります(too many clients alreadyエラー)。PgBouncer、Supabase Pooler、Neon Serverless Driverなどのコネクションプーリングソリューションを活用しましょう。
  • .returning()の積極的な活用:INSERTやUPDATEの後に、追加でSELECTクエリを発行するのは避けましょう。PostgreSQLのRETURNING *句を利用することで、1回のネットワークラウンドトリップで最新データを取得できます。
  • $inferSelectと$inferInsertを常にエクスポートする:これらの型をDTOやAPIレスポンスの標準型として利用します。スキーマに変更があった場合でも、TypeScriptコンパイラが関連するコードレイヤーのエラーを即座に検知してくれます。
Share: