TypeScriptでDrizzle ORMをマスターする:PostgreSQLでの型安全なデータベーススキーマ、マイグレーション、リレーショナルクエリ

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

背景とDrizzle ORMを選ぶ理由

TypeScriptでバックエンド開発をしたことがあるなら、同じ壁にぶつかったはずだ。データベース層とTypeScriptの型が別々の世界に存在している問題だ。インターフェースを書き、Prismaのモデルを書き、Zodスキーマを書く。そしてデータベースが変わると、3か所を追いかけて更新しなければならない。消耗するという言葉では足りないくらいだ。

Drizzle ORMはこの問題を根本から回避する。独自のスキーマ言語も、重いランタイムも不要だ。スキーマそのものがTypeScriptであり、テーブルを定義するだけで型が自動的に手に入る。コード生成のステップもなく、データベースが知っている情報とTypeScriptが知っている情報の間にズレが生じることもない。

私はこれを、1日約15万クエリを処理するSaaSプロジェクトの本番環境で運用している。6か月が経過したが、ランタイムでの型の想定外エラーはゼロ、マイグレーションの出力も予測通りで、クエリはテンプレートリテラルで包んだSQL文字列ではなくTypeScriptを書く感覚そのものだ。

型安全性を犠牲にせずSQLの可視性が必要なチームにとって、Drizzleは他に代わるものがない独自のポジションを占めている。必要なときはいつでも生のSQLに降りられる。ORMが邪魔をすることはない。この柔軟性は本当に珍しい。

際立っている点を挙げると:

  • TypeScriptファーストなスキーマ — テーブル定義はただのTypeScriptオブジェクト
  • drizzle-kit — スキーマの差分からSQLマイグレーションを生成・実行するCLIツール
  • リレーショナルクエリAPI — リレーションを一度宣言するだけで、JOINを手書きせずネストされたデータをクエリできる
  • 薄いランタイム — 魔法もなく、隠れたクエリもなく、予測可能なパフォーマンス

インストール

新しいTypeScriptプロジェクトから始めるか、既存のプロジェクトに組み込む。Node.js 18以降と稼働中のPostgreSQLインスタンスが必要だ。

依存関係のインストール

npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg tsx typescript
  • drizzle-orm — メインライブラリ
  • pg — PostgreSQLドライバ(Drizzleは複数のドライバをサポートしている)
  • drizzle-kit — マイグレーション用CLI
  • tsx — 開発中にTypeScriptファイルを直接実行できるツール

TypeScriptの初期化

npx tsc --init

tsconfig.jsonに少なくとも以下の設定が含まれていることを確認する:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "strict": true,
    "outDir": "./dist"
  }
}

設定

ステップ1 — データベース接続の定義

src/db/index.tsを作成して接続を保持し、dbインスタンスをエクスポートする:

import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

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

接続文字列を.envファイルに保存する:

DATABASE_URL=postgresql://user:password@localhost:5432/mydb

ステップ2 — スキーマの記述

src/db/schema.tsを開く(または作成する)。テーブル定義はシンプルなTypeScriptであり、デコレータも特別な構文も不要だ:

import { pgTable, serial, text, varchar, integer, timestamp } 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: text('email').notNull().unique(),
  createdAt: timestamp('created_at').defaultNow(),
});

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

次はリレーションの宣言だ。Drizzleがテーブルの結合方法を把握できるよう宣言しておけば、毎回指定する必要がなくなる:

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はカラム定義だけから完全な型形状を推論する。

ステップ3 — drizzle-kitの設定

プロジェクトルートにdrizzle.config.tsを追加する:

import type { Config } from 'drizzle-kit';

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

package.jsonにスクリプトを追加する:

{
  "scripts": {
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "db:studio": "drizzle-kit studio",
    "db:push": "drizzle-kit push"
  }
}

ステップ4 — マイグレーションの生成と実行

スキーマの準備ができたら、SQLマイグレーションを生成する:

npm run db:generate

Drizzleはスキーマを最後に記録された状態と比較し、./drizzle/配下にSQLファイルを書き出す。ファイルを開くと、実際に何が行われているかを隠す抽象化のない、クリーンで読みやすいSQLが確認できる。

その後、適用する:

npm run db:migrate

開発中は、db:pushを使えばマイグレーションファイルの生成ステップを省略してデータベースに直接変更を適用できる。マイグレーション履歴がまだ不要な段階での高速なイテレーションに便利だ。

ステップ5 — 型安全なクエリの記述

スキーマが接続されると、すべてのクエリに完全なTypeScript推論が適用される。src/index.tsを作成する:

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

// ユーザーを挿入する
const [newUser] = await db.insert(users).values({
  name: 'Alice',
  email: '[email protected]',
}).returning();

console.log(newUser.id); // TypeScriptはこれがnumber型であることを知っている

// そのユーザーに紐づいた投稿を挿入する
await db.insert(posts).values({
  title: '最初の投稿',
  body: 'Drizzle ORMからこんにちは',
  authorId: newUser.id,
});

// リレーションを使ってクエリする(手動JOINは不要)
const result = await db.query.users.findFirst({
  where: eq(users.id, newUser.id),
  with: {
    posts: true,
  },
});

console.log(result?.posts); // TypeScriptは完全な型形状を推論する

実行する:

npx tsx src/index.ts

db.query.*のwith APIは先ほど宣言したリレーションを使用する。内部ではDrizzleが効率的なJOINを生成し、フラットな行ではなくネストされたオブジェクトが返ってくる。

ステップ6 — 特定カラムのフィルタリングと選択

すべてのカラムを取得することはほとんどないだろう。Drizzleのselect APIは部分的な選択も型安全にしてくれる:

const titles = await db
  .select({ title: posts.title, author: users.name })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))
  .where(eq(users.name, 'Alice'));

// titlesの型は { title: string; author: string }[] として推論される

キャストも、as anyも不要だ。戻り値の型は選択した内容に合わせて自動的に絞り込まれる。

検証とモニタリング

Drizzle Studio — ビジュアルDBエクスプローラー

Drizzleにはローカルで起動できるブラウザベースのUIが付属している:

npm run db:studio

https://local.drizzle.studioを開く。行の閲覧、テーブルの確認、クエリの手動実行が可能で、開発中にマイグレーションが正しく適用されたことを確認するのに便利だ。

マイグレーション履歴の確認

適用済みのマイグレーションは__drizzle_migrationsテーブルで追跡される。クエリを実行して適用済みの内容を確認できる:

psql $DATABASE_URL -c "SELECT * FROM __drizzle_migrations ORDER BY created_at DESC LIMIT 10;"

./drizzle/配下の各ファイルは実行されると対応するエントリが記録される。テーブルに存在しない場合は、まだ適用されていない。

開発中のSQLクエリのログ出力

drizzle()にloggerを渡すことで、実際に実行されているSQLを確認できる:

export const db = drizzle(pool, {
  schema,
  logger: true, // すべてのSQLを標準出力に表示する
});

本番環境ではオフにすること。N+1問題を早期に発見するための最速の方法だ。同じクエリがループで繰り返されているのを見つけたら、ループの外に出すかバッチ処理に変更する必要がある。

スキーマ変更の安全な処理

カラムの型を変更したり、制約を追加したりする場合はdb:generateを再実行する。Drizzleは以前のスナップショットとの差分を計算し、インクリメンタルなマイグレーションファイルを生成する。

db:migrateを実行する前に生成されたSQLを確認すること。特にデータ損失を引き起こす可能性のあるカラムの削除や型変更には注意が必要だ:

# 適用前に変更内容をプレビューする
npm run db:generate
cat drizzle/0001_*.sql  # 生成されたSQLを確認する
npm run db:migrate      # 確認後に適用する

本番デプロイでは、アプリケーションの起動時ではなく、CI/CDパイプライン内でアプリケーション起動前にマイグレーションを実行すること。これによりデプロイがアトミックに保たれ、水平スケーリング時のレースコンディションを回避できる。

簡単な動作確認スクリプト

DBの接続とスキーマが同期していることを確認するシンプルなヘルスチェック:

// src/db/health.ts
import { db } from '.';
import { sql } from 'drizzle-orm';

export async function checkDb() {
  const result = await db.execute(sql`SELECT 1 AS ok`);
  console.log('DB接続済み:', result.rows[0]);
}

デプロイのたびに実行して、トラフィックを受け付ける前に接続を確認する。

次のステップ

このセットアップが稼働したら、次の自然なステップはトランザクションの追加(db.transaction())、実際の負荷に合わせた接続プールサイズの調整、高頻度クエリへのDrizzleのプリペアドステートメントの活用だ。これらはすべて公式ドキュメントで詳しく解説されている。

このセットアップを通じて最も印象的だったのは、データベースとTypeScriptの間のギャップが本当に小さくなることだ。スキーマのカラムを変更すると、TypeScriptはデータベースを触る前にそのカラムに依存するすべてのクエリにすぐフラグを立てる。その機能だけで、従来のセットアップなら本番環境でしか顕在化しなかったバグをいくつも事前に発見できた。

Share: