モダンなTypeScriptスタックでDrizzle ORMがPrismaに取って代わりつつある理由

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

重厚なデータベース抽象化が抱える問題

長年、TypeScript開発者にとってPrismaはデフォルトの選択肢でした。自動生成されるクライアントと堅牢な型安全性により、Node.jsのデータベース管理の悩みを見事に解決しました。

しかし、サーバーレスアーキテクチャが一般的になるにつれ、綻びが見え始めました。筆者は、Prismaの重厚なRustベースのクエリエンジンのせいで、Lambda関数が2秒ものコールドスタートに見舞われる場面を何度も目にしてきました。ビジネスロジックが1行も実行される前にデータベースレイヤーがそれほどのレイテンシを発生させているなら、スタックを再考すべき時です。

問題は「抽象化の溝」にあります。多くのORMは、独自のドメイン特化言語(DSL)の背後に SQLを隠そうとします。これは、DSLがサポートしていない複雑なJOINやウィンドウ関数が必要になるまでは機能します。しかし、必要になった途端、機能開発ではなくツールとの格闘が始まってしまいます。Drizzle ORMは異なる道を歩んでいます。これは標準的なSQLの上に構築された、薄い型安全なレイヤーです。基本的なSELECT文が書けるなら、Drizzleの使い方はすでに知っているも同然です。

クイックスタート:5分でクエリを実行するまで

Drizzleが新鮮なのは、グローバルなCLIや複雑な初期化を必要としない点です。それは単なるライブラリです。サイズを比較してみましょう。Prismaのエンジンがデプロイパッケージに15MB以上追加するのに対し、Drizzleはわずか15KB程度で、実質的にオーバーヘッドはありません。

まずはPostgreSQL用のコアパッケージをインストールします:

npm install drizzle-orm pg
npm install -D drizzle-kit @types/pg typescript

schema.tsファイルでデータ構造を定義します。構文はSQLのテーブル定義を反映していますが、完全にTypeScript内に留まります:

import { pgTable, serial, text, varchar, timestamp } from "drizzle-orm/pg-core";

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

データベースへの接続は簡単です。Drizzleは既存のドライバ(pgpostgres.jsなど)を使用し、バックグラウンドでのコード生成ステップなしで型安全性を提供します:

import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { users } from "./schema";

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

async function main() {
  const allUsers = await db.select().from(users);
  console.log("ユーザー数:", allUsers.length);
}

スキーマ設計と透明性の高いマイグレーション

Drizzleは「SQLファースト」の哲学に従っています。データベースで異なって見えるかもしれない抽象的なオブジェクトを定義するのではなく、データベースの状態を直接定義します。この透明性により、デバッグが大幅に容易になります。

リレーションシップの定義

Drizzleのリレーションシップは明示的です。これにより、データベースがブラックボックス化するのを防ぎます。マイグレーションスクリプトと同じように外部キーを定義し、ハードウェアレベルでデータの整合性を確保します。

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

export const posts = pgTable("posts", {
  id: serial("id").primaryKey(),
  title: text("title").notNull(),
  authorId: integer("author_id").references(() => users.id),
});

Drizzle KitによるSQLの自動化

マイグレーションの処理は、通常、デプロイの中で最もストレスのかかる部分です。Drizzle Kitは、TypeScriptのスキーマと実際のデータベースを比較することで、これを簡素化します。そして、標準的なSQLファイルを生成します。独自のJSONやXML形式を使用する他のツールとは異なり、Drizzleは読み取り、編集、バージョン管理が可能な生のSQLを提供します。

本番環境において、この可視性は大きなメリットです。データに触れる前に、どのALTER TABLEコマンドが実行されるかを正確に確認できます。マイグレーションを生成するには、次のコマンドを実行するだけです:

npx drizzle-kit generate

このコマンドは.sqlファイルを作成します。これをCI/CDパイプラインmigrateコマンドで適用したり、ローカル開発での迅速なプロトタイピングのためにnpx drizzle-kit pushを使用したりできます。

最適化されたクエリとプリペアドステートメント

DrizzleはSQLに近い構文を得意としますが、開発効率を高めるための「Relational Queries」APIも提供しています。このAPIは、ネストされたデータを単一の高度に最適化されたSQLクエリで取得することで、N+1クエリ問題を解決するために専用設計されています。

const usersWithPosts = await db.query.users.findMany({
  with: {
    posts: true,
  },
});

高トラフィックのエンドポイントで最大限のパフォーマンスが必要な場合、Drizzleは**Prepared Statements(プリペアドステートメント)**をサポートしています。これにより、データベースは実行計画を事前にコンパイルできます。筆者のテストでは、プリペアドステートメントを使用することで、繰り返されるリクエストの解析フェーズをスキップし、クエリのレイテンシを10〜15%削減できました。

const userQuery = db.select().from(users).where(eq(users.id, placeholder('id'))).prepare('userQuery');
await userQuery.execute({ id: 1 });

本番デプロイのためのプロの技

「魔法のような」ORMに慣れている場合、Drizzleへの切り替えには少し頭の切り替えが必要です。規模が拡大してもコードベースを綺麗に保つためのコツを以下に示します:

  • Zodを早期に統合する: drizzle-zodを使用して、テーブル定義からバリデーションスキーマを直接生成します。これにより、APIの型とデータベースの型を手動更新なしで完全に同期させることができます。
  • テーブルをモジュール化する: 50個のテーブルを1つのファイルに詰め込まないでください。ドメインごとに個別のファイルを持つschema/ディレクトリを作成し(例:billing.ts, auth.ts)、それらを中央のインデックスから再エクスポートします。
  • Strict Mode(厳格モード)を使用する: drizzle.config.tsstrict: trueを有効にします。これにより、エッジケースの処理が強制され、スキーマ変更時の予期せぬデータ損失を防ぐことができます。
  • SQLテンプレートリテラル: JSONBパスナビゲーションのような特定のPostgreSQL機能が必要な場合は、sqlオペレータを使用してください。これにより、型安全で合成可能な生のSQLスニペットを記述できます。

Drizzle ORMは、開発者の SQL知識を尊重しつつ、最高水準のTypeScript体験を提供するという、稀有なバランスを実現しています。遅いコールドスタートにうんざりしていたり、複雑なDSLに苦戦していたりするなら、Drizzleは論理的な次のステップです。高速で予測可能であり、開発の邪魔をしないため、機能構築に集中することができます。

Share: