🚀 Drizzle ORM 入門:TypeScriptのための次世代ORM

技術

近年のWeb開発、特にTypeScriptエコシステムにおいて、データベースとの連携は不可欠な要素です。その中で、ORM (Object-Relational Mapping) は、開発者がSQLを直接記述することなく、オブジェクト指向の考え方でデータベースを操作できるようにする強力なツールです。数あるORMの中でも、近年注目を集めているのが Drizzle ORM です。

Drizzle ORMは、TypeScriptのために設計された、軽量で高速、そして型安全なORMとして人気を博しています。本記事では、Drizzle ORMの基本的な概念から、セットアップ、スキーマ定義、CRUD操作、マイグレーション、そして他のORMとの比較まで、包括的に解説していきます。Drizzle ORMを初めて使う方や、概要を知りたい方にとって、有益な情報を提供できれば幸いです 😊。

Drizzle ORMは、TypeScript/JavaScriptアプリケーション向けに設計された、SQLデータベースを操作するためのORMです。公式ドキュメントでは「SQLを知っていればDrizzleもわかる (If you know SQL — you know Drizzle)」と謳われているように、SQLライクな構文を重視している点が大きな特徴です。これにより、SQLに慣れている開発者にとっては学習コストが低く、直感的にクエリを記述できます。

他の多くのORMがSQLを抽象化しようとするのに対し、DrizzleはSQLのパワーを最大限に活用しつつ、TypeScriptによる型安全性を加えるアプローチを取っています。依存関係がゼロで軽量であるため、パフォーマンスが高く、特にサーバーレス環境での利用にも適しています。

💡 ポイント: Drizzleは、ORMというよりはSQLライクなクエリビルダーとしての側面が強く、TypeScriptの型システムを最大限に活用して開発体験を高めることを目指しています。

Drizzle ORMが注目される理由は、そのユニークな特徴にあります。
  • 完全な型安全性 (Full Type Safety): TypeScriptネイティブであり、スキーマ定義からクエリ結果まで、強力な型推論と型チェックを提供します。これにより、コンパイル時にエラーを検出し、ランタイムエラーのリスクを大幅に削減します。
  • SQLライクな構文 (SQL-like Syntax): SQLの知識があれば容易に学習・利用できます。複雑なクエリも直感的に記述可能です。他のORMのように独自のDSL(ドメイン固有言語)を深く学ぶ必要が少ない点がメリットです。
  • 軽量性とパフォーマンス (Lightweight & Performance): 依存関係ゼロで設計されており、非常に軽量です。ランタイムのオーバーヘッドが少なく、高速なクエリ実行を実現します。常に1つのSQLクエリを出力するため、特にサーバーレス環境でのパフォーマンスやコスト面で有利です。
  • Drizzle Kit (マイグレーションツール): スキーマ定義ファイルからSQLマイグレーションファイルを自動生成するCLIツール `drizzle-kit` が提供されています。これにより、データベーススキーマの変更管理が容易になります。
  • 柔軟なデータベースサポート: PostgreSQL, MySQL, SQLiteといった主要なリレーショナルデータベースをサポートしています。また、LibSQL/Turso, Neon, PlanetScale, TiDBなど、新しいデータベースやプラットフォームへの対応も積極的に行われています。
  • リレーショナルクエリ (Relational Queries): テーブル間のリレーションを定義し、関連データを簡単に取得するための簡潔なAPI (`findMany`, `findOne`) も提供されています。これにより、複雑なJOIN操作をより抽象的に記述できます。
  • Zodスキーマ生成: スキーマ定義からZodスキーマを生成する機能があり、バリデーションの実装を容易にします。
  • Drizzle Studio: データベースの構造やデータをGUIで視覚的に確認・操作できるローカルツールです。開発中のデバッグやデータ確認に役立ちます。

🎉 これらの特徴により、Drizzle ORMは開発者に快適な開発体験と高いパフォーマンスを提供します。

Drizzle ORMをプロジェクトに導入するのは簡単です。ここでは、PostgreSQLを使用する場合の基本的な手順を示します。

1. 必要なパッケージのインストール

まず、Drizzle ORM本体と、使用するデータベースドライバ(ここではPostgreSQL用の`pg`または`postgres`)、そしてマイグレーションツール`drizzle-kit`をインストールします。TypeScriptを使用するため、型定義ファイルもインストールしましょう。

Node-Postgres (`pg`) を使用する場合:

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

Postgres.js (`postgres`) を使用する場合:

npm install drizzle-orm postgres
npm install -D drizzle-kit typescript ts-node

プロジェクトに合わせて `yarn` や `pnpm`, `bun` を使用することもできます。

2. TypeScript設定 (`tsconfig.json`)

基本的なTypeScript設定ファイルを作成します。

{
  "compilerOptions": {
    "target": "ES2016",
    "module": "CommonJS",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

3. Drizzle設定ファイル (`drizzle.config.ts`)

`drizzle-kit` がマイグレーションなどの操作を行う際に参照する設定ファイルを作成します。プロジェクトのルートディレクトリに配置するのが一般的です。

import type { Config } from "drizzle-kit";
import * as dotenv from "dotenv";

dotenv.config({ path: '.env' }); // 環境変数を読み込む

export default {
  dialect: "postgresql", // 'mysql2', 'sqlite', 'postgresql'
  schema: "./src/db/schema.ts", // スキーマファイルのパス
  out: "./drizzle", // マイグレーションファイルの出力先ディレクトリ
  dbCredentials: {
    // 環境変数からデータベース接続情報を取得
    url: process.env.DATABASE_URL!,
  },
  // verbose: true, // 詳細なログを出力する場合
  // strict: true,  // 厳格モードを有効にする場合
} satisfies Config;

データベース接続情報は、セキュリティのために `.env` ファイルなどで管理し、`dotenv` パッケージなどを使って読み込むのが良いでしょう。

# .env ファイルの例
DATABASE_URL="postgresql://user:password@host:port/database"

4. データベース接続 (`src/db/index.ts`)

データベースへの接続を確立し、Drizzleインスタンスを作成するファイルを用意します。

Node-Postgres (`pg`) を使用する場合:

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

dotenv.config({ path: '.env' });

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

// スキーマ定義をインポートすることも可能(リレーショナルクエリで必要)
// import * as schema from './schema';
// export const db = drizzle(pool, { schema });

export const db = drizzle(pool);

Postgres.js (`postgres`) を使用する場合:

import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as dotenv from "dotenv";

dotenv.config({ path: '.env' });

const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString);

// スキーマ定義をインポートすることも可能(リレーショナルクエリで必要)
// import * as schema from './schema';
// export const db = drizzle(client, { schema });

export const db = drizzle(client);

これで、Drizzle ORMを使用するための基本的な準備が整いました ✨。

Drizzleでは、TypeScriptのコードとしてデータベースのスキーマ(テーブル、カラム、リレーションなど)を定義します。これにより、SQLのCREATE TABLE文に近い感覚で、かつ型安全にスキーマを記述できます。

スキーマ定義は通常、`src/db/schema.ts` のようなファイルに記述します。

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

// Enumの定義 (PostgreSQLのENUM型に対応)
export const userRoleEnum = pgEnum('user_role', ['admin', 'editor', 'viewer']);

// users テーブル
export const users = pgTable('users', {
  id: serial('id').primaryKey(), // 自動増分する主キー
  fullName: text('full_name').notNull(),
  email: varchar('email', { length: 256 }).unique().notNull(), // 一意制約付き
  role: userRoleEnum('role').default('viewer'), // Enum型、デフォルト値付き
  createdAt: timestamp('created_at').defaultNow().notNull(), // デフォルトで現在時刻
  updatedAt: timestamp('updated_at').defaultNow().$onUpdate(() => new Date()), // 更新時に現在時刻
}, (table) => {
  return {
    // インデックスの定義
    emailIndex: uniqueIndex('email_idx').on(table.email),
  };
});

// posts テーブル
export const posts = pgTable('posts', {
  id: serial('id').primaryKey(),
  title: varchar('title', { length: 256 }).notNull(),
  content: text('content'),
  authorId: integer('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }), // 外部キー制約 (users.idを参照、削除時カスケード)
  published: boolean('published').default(false).notNull(),
  publishedAt: timestamp('published_at'),
});

// categories テーブル
export const categories = pgTable('categories', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 100 }).unique().notNull(),
});

// posts_to_categories テーブル (多対多リレーション用の中間テーブル)
export const postsToCategories = pgTable('posts_to_categories', {
  postId: integer('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
  categoryId: integer('category_id').notNull().references(() => categories.id, { onDelete: 'cascade' }),
}, (table) => {
  return {
    // 複合主キーの定義
    pk: primaryKey({ columns: [table.postId, table.categoryId] }),
  };
});

// --- リレーションの定義 (Relational Queries用) ---

// usersとpostsのリレーション (1対多)
export const usersRelations = relations(users, ({ many }) => ({
  posts: many(posts), // userは複数のpostを持つ
}));

// postsとusersのリレーション (多対1)
export const postsRelations = relations(posts, ({ one, many }) => ({
  author: one(users, { // postは1人のauthorを持つ
    fields: [posts.authorId], // postsテーブルのauthorIdカラム
    references: [users.id],   // usersテーブルのidカラム
  }),
  postsToCategories: many(postsToCategories), // postは複数の中間テーブルレコードを持つ
}));

// categoriesとpostsToCategoriesのリレーション (1対多)
export const categoriesRelations = relations(categories, ({ many }) => ({
  postsToCategories: many(postsToCategories), // categoryは複数の中間テーブルレコードを持つ
}));

// postsToCategoriesとposts, categoriesのリレーション (多対1)
export const postsToCategoriesRelations = relations(postsToCategories, ({ one }) => ({
  post: one(posts, { // 中間テーブルレコードは1つのpostを持つ
    fields: [postsToCategories.postId],
    references: [posts.id],
  }),
  category: one(categories, { // 中間テーブルレコードは1つのcategoryを持つ
    fields: [postsToCategories.categoryId],
    references: [categories.id],
  }),
}));

// スキーマから型を推論 (クエリ結果などで利用)
export type User = typeof users.$inferSelect; // SELECT時の型
export type NewUser = typeof users.$inferInsert; // INSERT時の型
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
export type Category = typeof categories.$inferSelect;
export type NewCategory = typeof categories.$inferInsert;
  • pgTable (または mysqlTable, sqliteTable) でテーブルを定義します。
  • カラムの型 (serial, text, varchar, integer, timestamp, boolean など) と制約 (primaryKey, notNull, unique, default, references など) を指定します。
  • pgEnum などでデータベース固有の型も定義できます。
  • テーブル定義の第3引数でインデックス (uniqueIndex, index) や複合主キー (primaryKey) を定義できます。
  • relations 関数を使って、テーブル間のリレーションを定義します。これは後述するリレーショナルクエリで役立ちます。one は多対一または一対一、many は一対多または多対多のリレーションを示します。
  • $inferSelect$inferInsert を使うと、スキーマ定義からTypeScriptの型を自動的に推論でき、コード全体で型安全性を保つのに役立ちます。

⚠️ スキーマ定義はデータベースの構造そのものを表します。慎重に設計し、変更履歴は後述するマイグレーションで管理しましょう。

スキーマ定義ができたら、いよいよデータベース操作です。Drizzle ORMはSQLライクなAPIを提供しており、直感的にCRUD (Create, Read, Update, Delete) 操作を行えます。

ここでは、先ほど定義したスキーマ (`users`, `posts` など) とデータベース接続 (`db`) を使って、基本的な操作例を示します。

1. データ作成 (INSERT)

import { db } from './db/index'; // Drizzleインスタンス
import { users, posts, NewUser, NewPost } from './db/schema'; // スキーマ定義

async function createUser() {
  const newUser: NewUser = {
    fullName: 'Alice Wonderland',
    email: 'alice@example.com',
    role: 'admin', // Enum型もそのまま使える
  };

  // 単一レコードの挿入
  const insertedUsers = await db.insert(users)
    .values(newUser)
    .returning(); // 挿入されたレコード全体を返す (PostgreSQL)

  console.log('Inserted User:', insertedUsers[0]);

  // 複数レコードの挿入
  const newUsers: NewUser[] = [
    { fullName: 'Bob The Builder', email: 'bob@example.com' },
    { fullName: 'Charlie Chaplin', email: 'charlie@example.com', role: 'editor' },
  ];

  const insertedMultiple = await db.insert(users)
    .values(newUsers)
    .returning({ insertedId: users.id, email: users.email }); // 特定のカラムのみ返す

  console.log('Inserted Multiple Users:', insertedMultiple);
}

async function createPost() {
  // 既存のユーザーIDを取得 (例)
  const alice = await db.select({ id: users.id }).from(users).where(eq(users.email, 'alice@example.com')).limit(1);
  if (!alice.length) {
    console.error('User Alice not found');
    return;
  }
  const aliceId = alice[0].id;

  const newPost: NewPost = {
    title: 'My First Blog Post',
    content: 'This is the content of my first post.',
    authorId: aliceId,
    published: true,
    publishedAt: new Date(),
  };

  const insertedPost = await db.insert(posts)
    .values(newPost)
    .returning();

  console.log('Inserted Post:', insertedPost[0]);
}

createUser();
createPost();
  • db.insert(テーブル名) でINSERT操作を開始します。
  • .values(データ) で挿入するデータを指定します。単一オブジェクトまたはオブジェクトの配列を渡せます。
  • .returning() (PostgreSQLなど一部DB) で挿入されたレコードを取得できます。引数で返すカラムを指定することも可能です。
  • 事前に定義した NewUser 型などを使うと、挿入データの型安全性が保証されます。

2. データ取得 (SELECT)

import { db } from './db/index';
import { users, posts } from './db/schema';
import { eq, ne, gt, lt, gte, lte, like, ilike, and, or, sql, asc, desc, count } from 'drizzle-orm'; // 条件式や関数

async function getUsers() {
  // 全ユーザー取得
  const allUsers = await db.select().from(users);
  console.log('All Users:', allUsers);

  // 特定のカラムのみ取得
  const userEmails = await db.select({ email: users.email, name: users.fullName }).from(users);
  console.log('User Emails:', userEmails);

  // 条件指定 (emailが 'alice@example.com' のユーザー)
  const alice = await db.select().from(users).where(eq(users.email, 'alice@example.com'));
  console.log('Alice:', alice[0]);

  // 複数の条件 (roleが 'admin' で、IDが1より大きい)
  const admins = await db.select().from(users).where(and(
    eq(users.role, 'admin'),
    gt(users.id, 1)
  ));
  console.log('Admins:', admins);

  // OR 条件 (roleが 'admin' または 'editor')
  const editorsOrAdmins = await db.select().from(users).where(or(
    eq(users.role, 'admin'),
    eq(users.role, 'editor')
  ));
  console.log('Editors or Admins:', editorsOrAdmins);

  // LIKE検索 (名前に 'li' を含む、大文字小文字区別なし)
  const usersWithLi = await db.select().from(users).where(ilike(users.fullName, '%li%'));
  console.log('Users with "li":', usersWithLi);

  // 並び替え (createdAtの降順)
  const sortedUsers = await db.select().from(users).orderBy(desc(users.createdAt));
  console.log('Sorted Users:', sortedUsers);

  // ページネーション (2件目から3件取得)
  const paginatedUsers = await db.select().from(users).limit(3).offset(1);
  console.log('Paginated Users:', paginatedUsers);

  // 集計関数 (ユーザー総数)
  const userCountResult = await db.select({ value: count() }).from(users);
  console.log('Total Users:', userCountResult[0].value);

  // SQL関数や生SQLの使用
  const userWithUpperEmail = await db.select({
    id: users.id,
    upperEmail: sql`upper(${users.email})` // 型を指定
  }).from(users).limit(1);
  console.log('User with upper email:', userWithUpperEmail[0]);
}

getUsers();
  • db.select() または db.select({ カラム指定 }) でSELECT操作を開始します。
  • .from(テーブル名) で対象テーブルを指定します。
  • .where(条件式) で取得条件を指定します。eq (等しい), ne (等しくない), gt (より大きい), lt (より小さい), gte (以上), lte (以下), like, ilike (部分一致), inArray, isNull など、豊富な条件演算子が用意されています。
  • and(), or() で複数の条件を組み合わせられます。
  • .orderBy() で並び順を指定します (asc: 昇順, desc: 降順)。
  • .limit(), .offset() で取得件数と開始位置を指定し、ページネーションを実現します。
  • count(), avg(), sum(), min(), max() などの集計関数も利用できます。
  • sql テンプレートリテラルを使えば、型安全性を維持しつつ生SQLを埋め込むことも可能です。

3. データ更新 (UPDATE)

import { db } from './db/index';
import { users } from './db/schema';
import { eq } from 'drizzle-orm';

async function updateUser() {
  const emailToUpdate = 'bob@example.com';
  const newRole = 'admin';

  // 特定のユーザーのroleを更新
  const updatedUsers = await db.update(users)
    .set({
      role: newRole,
      updatedAt: new Date() // updatedAtも手動で更新
    })
    .where(eq(users.email, emailToUpdate))
    .returning({ updatedId: users.id, updatedRole: users.role }); // 更新されたレコードの情報を返す

  if (updatedUsers.length > 0) {
    console.log('Updated User:', updatedUsers[0]);
  } else {
    console.log(`User with email ${emailToUpdate} not found.`);
  }

  // 全ユーザーのupdatedAtを更新 (注意して実行)
  // await db.update(users).set({ updatedAt: new Date() });
}

updateUser();
  • db.update(テーブル名) でUPDATE操作を開始します。
  • .set({ 更新データ }) で更新するカラムと値を指定します。
  • .where(条件式) で更新対象のレコードを絞り込みます。
  • .returning() (PostgreSQLなど) で更新されたレコードの情報を取得できます。

4. データ削除 (DELETE)

import { db } from './db/index';
import { users } from './db/schema';
import { eq } from 'drizzle-orm';

async function deleteUser() {
  const emailToDelete = 'charlie@example.com';

  // 特定のユーザーを削除
  const deletedUsers = await db.delete(users)
    .where(eq(users.email, emailToDelete))
    .returning({ deletedId: users.id }); // 削除されたレコードのIDを返す

  if (deletedUsers.length > 0) {
    console.log('Deleted User ID:', deletedUsers[0].deletedId);
  } else {
    console.log(`User with email ${emailToDelete} not found.`);
  }

  // 全ユーザーを削除 (非常に危険!注意!)
  // await db.delete(users);
}

deleteUser();
  • db.delete(テーブル名) でDELETE操作を開始します。
  • .where(条件式) で削除対象のレコードを絞り込みます。
  • .returning() (PostgreSQLなど) で削除されたレコードの情報を取得できます。

5. トランザクション

複数の操作をアトミックに行いたい場合は、トランザクションを使用します。

import { db } from './db/index';
import { users, posts, NewUser } from './db/schema';

async function transactionExample() {
  try {
    await db.transaction(async (tx) => {
      // トランザクション内で操作を行う (txオブジェクトを使用)
      const newUser: NewUser = { fullName: 'David Copperfield', email: 'david@example.com' };
      const insertedUser = await tx.insert(users).values(newUser).returning({ id: users.id });
      const userId = insertedUser[0].id;

      console.log('User created within transaction:', userId);

      const newPost = { title: 'Magic Post', authorId: userId };
      await tx.insert(posts).values(newPost);

      console.log('Post created within transaction');

      // 何らかの理由でロールバックしたい場合
      // throw new Error("Something went wrong, rollback!");
    });
    console.log('Transaction committed successfully! ✅');
  } catch (error) {
    console.error('Transaction failed and rolled back! ❌', error);
  }
}

transactionExample();
  • db.transaction(async (tx) => { ... }) を使用します。
  • コールバック関数内の処理はすべて同一トランザクション内で実行されます。
  • コールバック関数内でエラーが発生すると、トランザクション全体が自動的にロールバックされます。
  • トランザクション内の操作では、引数として渡される `tx` オブジェクト(`db` と同じインターフェースを持つ)を使用します。

リレーショナルデータベースの強力な機能の一つが、テーブル間のリレーションです。Drizzle ORMでは、標準のSQLライクなJOIN操作と、より抽象化されたリレーショナルクエリ (Relational Queries) の両方でリレーションを扱うことができます。

1. SQLライクなJOIN操作

標準的なJOIN (LEFT JOIN, INNER JOIN, RIGHT JOIN, FULL JOIN) を使って、複数のテーブルを結合してデータを取得できます。

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

async function getPostsWithAuthors() {
  // INNER JOIN: postsとusersをauthorIdで結合し、両方のテーブルに存在するレコードのみ取得
  const postsWithAuthorsInner = await db.select({
    postId: posts.id,
    postTitle: posts.title,
    authorName: users.fullName,
    authorEmail: users.email,
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id)); // eq(左テーブルのカラム, 右テーブルのカラム)

  console.log('Posts with Authors (INNER JOIN):', postsWithAuthorsInner);

  // LEFT JOIN: usersを基準にpostsを結合。ユーザーに紐づく投稿がない場合でもユーザー情報は取得される
  const usersWithPostsLeft = await db.select({
    userId: users.id,
    userName: users.fullName,
    postId: posts.id, // 投稿がない場合はnullになる可能性がある
    postTitle: posts.title,
  })
  .from(users)
  .leftJoin(posts, eq(users.id, posts.authorId));

  console.log('Users with Posts (LEFT JOIN):', usersWithPostsLeft);

  // 複数テーブルのJOIN (例: posts -> users -> hypothetical_profiles)
  // const complexJoin = await db.select(...)
  //  .from(posts)
  //  .innerJoin(users, eq(posts.authorId, users.id))
  //  .leftJoin(profiles, eq(users.id, profiles.userId)) // さらに別のテーブルをJOIN
  //  .where(...)
}

getPostsWithAuthors();
  • .innerJoin(), .leftJoin(), .rightJoin(), .fullJoin() メソッドを使用します。
  • 第1引数に結合するテーブル、第2引数に結合条件 (通常は `eq()`) を指定します。
  • select() で複数のテーブルのカラムを指定できます。カラム名が重複する場合は、エイリアスを使うか、明示的に { tableName.columnName } のように指定する必要があります (Drizzleが自動で解決してくれる場合もあります)。

2. リレーショナルクエリ (Relational Queries)

スキーマ定義時に `relations` 関数でリレーションを定義しておくと、よりオブジェクト指向的な方法で関連データを取得できます。JOINを明示的に書く必要がなくなり、コードが簡潔になることがあります。この機能を使うには、Drizzleインスタンスを作成する際にスキーマを渡す必要があります。

// db/index.ts でスキーマを渡す設定に変更
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema'; // スキーマ全体をインポート
import * as dotenv from "dotenv";

dotenv.config({ path: '.env' });

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

export const db = drizzle(pool, { schema }); // スキーマを渡す!
import { db } from './db/index';
import { users, posts, categories } from './db/schema'; // テーブル定義も必要に応じてインポート
import { eq } from 'drizzle-orm';

async function relationalQueriesExample() {
  // 特定のユーザーとその投稿一覧を取得 (1対多)
  const userWithPosts = await db.query.users.findFirst({
    where: eq(users.email, 'alice@example.com'),
    with: {
      posts: true, // usersRelationsで定義した 'posts' を取得
      // posts: { columns: { title: true } } // 投稿の特定カラムのみ取得も可能
    }
  });

  if (userWithPosts) {
    console.log('User with Posts:', userWithPosts.fullName);
    userWithPosts.posts.forEach(post => {
      console.log(`  - Post Title: ${post.title}`);
    });
  }

  // 特定の投稿とその著者を取得 (多対1)
  const postWithAuthor = await db.query.posts.findFirst({
    where: eq(posts.title, 'My First Blog Post'),
    with: {
      author: { // postsRelationsで定義した 'author' を取得
        columns: { // 著者の特定のカラムのみ取得
          fullName: true,
          email: true,
        }
      }
    }
  });

  if (postWithAuthor) {
    console.log('Post with Author:', postWithAuthor.title);
    console.log(`  - Author Name: ${postWithAuthor.author.fullName}`);
  }

  // 多対多のリレーション (投稿とカテゴリ)
  const postWithCategories = await db.query.posts.findFirst({
    where: eq(posts.id, 1), // 例: IDが1の投稿
    with: {
      postsToCategories: { // 中間テーブル経由
        with: {
          category: { // カテゴリ情報を取得
            columns: { name: true }
          }
        }
      }
    }
  });

  if (postWithCategories) {
    console.log('Post with Categories:', postWithCategories.title);
    postWithCategories.postsToCategories.forEach(ptc => {
      console.log(`  - Category: ${ptc.category.name}`);
    });
  }

  // findMany を使って複数レコードを取得
  const allUsersWithPosts = await db.query.users.findMany({
    limit: 5,
    orderBy: desc(users.createdAt),
    with: {
      posts: {
        limit: 3, // 各ユーザーごとに最新3件の投稿を取得
        orderBy: desc(posts.publishedAt),
        columns: { title: true, published: true }
      }
    }
  });
  console.log('All Users with Posts (findMany):', JSON.stringify(allUsersWithPosts, null, 2));

}

relationalQueriesExample();
  • db.query.テーブル名 でリレーショナルクエリを開始します。
  • findFirst() は条件に一致する最初の1件、findMany() は条件に一致する複数件を取得します。
  • where で取得条件を指定します (SQLライクな条件式が使えます)。
  • with オプションで、`relations` で定義した関連データを指定して取得します。ネストして関連データの関連データを取得することも可能です。
  • with 内で `columns` を指定すれば、関連データの特定のカラムのみを選択できます。
  • findMany では limit, offset, orderBy なども指定できます。with 内でも同様のオプションが指定できる場合があります。

💡 リレーショナルクエリは、特にネストされたデータの取得や、JOIN構文を意識したくない場合に便利です。ただし、内部的にはJOINが実行されているため、パフォーマンスへの影響を考慮する必要はあります。Drizzleは常に1つのSQLクエリを生成することを目指しています。

アプリケーション開発を進める中で、データベースのスキーマ(テーブル構造)を変更する必要が出てくることはよくあります。例えば、新しいカラムを追加したり、既存のカラムの型を変更したり、新しいテーブルを追加したりする場合です。このようなスキーマ変更を安全かつ確実に管理するための仕組みが「マイグレーション」です。

Drizzle ORMでは、Drizzle Kit というCLIツールを使ってマイグレーションを管理します。Drizzle Kitは、TypeScriptで記述されたスキーマ定義 (`schema.ts`) と、現在のデータベースの状態を比較し、差分を反映するためのSQLファイルを自動生成してくれます。

マイグレーションの基本的な流れ

  1. スキーマファイル (`schema.ts`) を変更する: カラム追加、テーブル追加、型変更など、目的の変更をスキーマ定義ファイルに反映させます。
  2. マイグレーションファイルを生成する (`generate`): Drizzle Kitのコマンドを実行して、変更内容に基づいたSQLマイグレーションファイルを生成します。
  3. マイグレーションを適用する (`migrate` または `push`): 生成されたSQLファイルをデータベースに適用して、実際のスキーマを変更します。

Drizzle Kit コマンド

よく使うDrizzle Kitのコマンドを見ていきましょう。これらのコマンドは `package.json` の `scripts` に登録しておくと便利です。

// package.json の例
{
  "scripts": {
    "db:generate": "drizzle-kit generate", // マイグレーションファイル生成
    "db:migrate": "drizzle-kit migrate",   // マイグレーション適用 (履歴管理あり)
    "db:push": "drizzle-kit push",       // スキーマを直接DBに反映 (履歴管理なし、開発初期向け)
    "db:studio": "drizzle-kit studio"    // Drizzle Studio起動
  }
}

1. `drizzle-kit generate`

現在のスキーマ定義 (`schema.ts`) と、前回のマイグレーション状態(またはDBの現状)を比較し、差分を適用するためのSQLファイル(例: `drizzle/0001_abc.sql`)を生成します。

npm run db:generate
# または
# npx drizzle-kit generate

このコマンドを実行すると、`drizzle.config.ts` で指定した `out` ディレクトリ(例: `./drizzle`)にSQLファイルとメタ情報が生成されます。生成されたSQLファイルの内容を確認し、意図した変更になっているかレビューすることが重要です。

-- 例: drizzle/0001_add_bio_to_users.sql
ALTER TABLE "users" ADD COLUMN "bio" text;
⚠️ `generate` コマンドはSQLファイルを生成するだけで、データベースにはまだ変更を適用しません。

2. `drizzle-kit migrate`

`generate` で生成されたSQLマイグレーションファイルのうち、まだ適用されていないものをデータベースに適用します。このコマンドは、どのマイグレーションが適用済みかをデータベース内の特別なテーブル(デフォルト: `__drizzle_migrations`)で管理します。これにより、複数人での開発や複数環境へのデプロイ時にも、安全にスキーマ変更を適用できます。

npm run db:migrate
# または
# npx drizzle-kit migrate

実行すると、未適用のSQLファイルが順番に実行され、`__drizzle_migrations` テーブルに適用記録が残ります。

💡 チーム開発や本番環境へのデプロイでは、この `migrate` コマンドを使うのが一般的です。マイグレーション履歴が残るため、ロールバック(手動でのSQL実行が必要な場合あり)や状態管理がしやすくなります。

3. `drizzle-kit push`

現在のスキーマ定義 (`schema.ts`) を、マイグレーションファイルを生成・管理することなく、直接データベースに反映させます。データベースの現状と `schema.ts` を比較し、差分を適用するためのSQLを内部的に生成して即座に実行します。

npm run db:push
# または
# npx drizzle-kit push

このコマンドは手軽ですが、マイグレーション履歴が残りません。そのため、スキーマを頻繁に変更する開発初期段階や、個人開発でのプロトタイピングには便利ですが、チーム開発や本番環境での運用には `generate` と `migrate` の組み合わせが推奨されます。

🚨 `push` コマンドは破壊的な変更(カラム削除など)を警告なしに行う可能性があるため、使用には注意が必要です。データの損失リスクがあります。

4. `drizzle-kit studio`

ローカルで動作するGUIツール Drizzle Studio を起動します。ブラウザでデータベースのテーブル構造を確認したり、データを直接表示・編集したりできます。開発中のデバッグに非常に役立ちます。

npm run db:studio
# または
# npx drizzle-kit studio

起動後、通常は `http://local.drizzle.studio` などにアクセスします。

マイグレーション戦略の選択

方法 コマンド 特徴 適したケース
コードファースト (履歴管理あり) generatemigrate
  • スキーマ変更履歴がSQLファイルとして残る
  • データベースに適用履歴が記録される
  • チーム開発、本番運用向け
通常の開発フロー、複数人開発、ステージング・本番環境
コードファースト (履歴管理なし) push
  • 手軽にスキーマをDBに反映
  • マイグレーションファイル不要
  • 変更履歴は残らない
  • データ損失リスクあり
開発初期、プロトタイピング、個人開発
データベースファースト (introspect) introspect
  • 既存のデータベーススキーマから schema.ts を生成
  • 既存DBにDrizzleを導入する場合
既存プロジェクトへのDrizzle導入

ほとんどの場合、`generate` と `migrate` を組み合わせる「コードファースト(履歴管理あり)」のアプローチが推奨されます。

TypeScriptエコシステムには、Drizzle ORM以外にも優れたORMが存在します。特に人気が高いのが Prisma です。DrizzleとPrismaはどちらも型安全なデータベースアクセスを提供しますが、アプローチや特徴には違いがあります。どちらを選択するかは、プロジェクトの要件やチームの好みに依存します。

特徴 Drizzle ORM Prisma
スキーマ定義 TypeScriptコード (`.ts`ファイル) 独自スキーマ言語 (PSL) (`.prisma`ファイル)
アプローチ コードファースト / SQLライク スキーマファースト / 抽象化されたAPI
型安全性 TypeScriptの型推論に依存 (リアルタイム) スキーマファイルから型生成 (`prisma generate`) が必要
クエリAPI SQLに近い構文 + リレーショナルクエリAPI オブジェクト指向的で抽象化されたAPI
学習曲線 SQL知識があれば低い PSLとPrisma Client APIの学習が必要 (ただし直感的)
マイグレーション Drizzle Kit (`generate`, `migrate`, `push`)。`migrate`はSQLファイル生成・適用。`push`は直接反映。 Prisma Migrate (`db push`, `migrate dev`, `migrate deploy`)。マイグレーション履歴管理が強力。
パフォーマンス 軽量で高速。依存関係ゼロ。常に1クエリ出力。サーバーレス向き。 比較的高速だが、Drizzleよりオーバーヘッドが大きい可能性。Rust製クエリエンジン。
柔軟性 SQLに近い分、細かい制御がしやすい。生SQLも容易。 APIが抽象化されている分、特定の複雑なクエリで制限がある場合も (ただし拡張機能あり)。
開発体験 (DX) 型推論がリアルタイム。SQL好きには快適。Drizzle Studioが便利。 `prisma generate` が必要だが、強力な型補完とAPI。Prisma Studio, Data Proxyなどのエコシステムが充実。
エコシステム/成熟度 比較的新しい (2022年頃~) が急速に成長中。 より成熟しており (2016年~)、コミュニティや導入事例が多い。MongoDBやMSSQLもサポート。

どちらを選ぶべきか? 🤔

Drizzle ORM が適しているケース:

  • SQLに慣れ親しんでおり、SQLライクな構文で開発したい。
  • 軽量性やパフォーマンスを最重視する(特にサーバーレス環境)。
  • データベースの細かい制御を行いたい。
  • `generate` ステップなしのリアルタイムな型推論を好む。
  • 比較的新しい技術を積極的に採用したい。

Prisma が適しているケース:

  • SQLをあまり書きたくない、より抽象化されたAPIを好む。
  • 強力なマイグレーション管理機能や充実したエコシステム(Prisma Studio, Data Proxy等)を活用したい。
  • スキーマファーストのアプローチが好み。
  • 複雑なリレーション操作をより簡潔に書きたい。
  • 成熟し、多くの導入実績があるツールを使いたい。
  • MongoDBやMSSQLなど、DrizzleがサポートしていないDBを使いたい。

Drizzle ORMは、TypeScript開発者にとって強力かつ魅力的な選択肢となるORMです。その主な特徴である SQLライクな構文完全な型安全性、そして 軽量性とパフォーマンス は、多くのプロジェクトでメリットをもたらすでしょう。

特に、SQLの知識を活かしたい開発者や、サーバーレス環境でのパフォーマンスを重視する場合には、Drizzle ORMは非常に有力な候補となります。Drizzle Kitによるマイグレーション管理や、リレーショナルクエリによる簡潔なデータ取得も、開発効率の向上に貢献します。

一方で、Prismaのようなスキーマファーストのアプローチや、より抽象化されたAPI、成熟したエコシステムを好む場合には、他の選択肢も検討する価値があります。

本記事を通して、Drizzle ORMの基本的な使い方や特徴、そして他のORMとの違いについて理解を深めていただけたなら幸いです。ぜひ、あなたの次のプロジェクトでDrizzle ORMを試してみてはいかがでしょうか?きっとその快適な開発体験とパフォーマンスに満足するはずです!🚀

コメント

タイトルとURLをコピーしました