Drizzle ORM から PostgreSQL を使用する際に、環境変数からschemaを切り替える
はじめに🔗
PostgreSQL には Schema(スキーマ) という概念があり、1 つのデータベース内に「名前空間」を作ってテーブルを管理できます。デフォルトでは public スキーマが使われますが、これを切り替えることで、たとえば「テナントごとにスキーマを分ける」「テスト実行時だけ隔離されたスキーマを使う」といった運用が可能になります。
Prisma ユーザーにはおなじみですが、PostgreSQL の接続 URL に ?schema=my_schema というパラメータを付けるだけで、接続先スキーマを切り替えられる機能があります。
しかし、これはあくまで Prisma が独自に実装している機能(糖衣構文)であり、PostgreSQL のドライバ標準の仕様ではありません。そのため、他の ORM やツールでは同様に記述しても無視されてしまいます。
Drizzle ORM も例外ではなく、通常の方法(pgSchema でのハードコーディング)では動的なスキーマ切り替えが困難です。本記事では、Drizzle ORM において環境変数(接続 URL)から動的にスキーマを切り替える実装方法を紹介します。
サンプルプロジェクト🔗
本記事で解説するコードの完成形は、以下のリポジトリで公開しています。
https://github.com/SoraKumo001/drizzle-pg-schema
使用技術🔗
- Drizzle ORM / Drizzle Kit: ORM およびマイグレーションツール
- PostgreSQL: データベース
- Docker: 実行環境
課題:なぜ標準機能だけでは足りないのか🔗
Drizzle ORM で特定のスキーマを利用する場合、通常はスキーマ定義ファイル内で pgSchema("my_schema") を使って固定的に記述する必要があります。しかし、これでは開発・テスト・本番でスキーマ名を柔軟に変えたいニーズに対応できません。
PostgreSQL 自体には search_path という設定があり、これをセッション接続時に指定することでデフォルトのスキーマを変更できます。今回はこの search_path を活用しつつ、Drizzle Kit の CLI コマンド(migrate や push)では対応しきれない部分をカスタムスクリプトで補うことで解決します。
実装のポイント🔗
実装の核となるのは以下の 3 点です。
drizzle.config.ts: 環境変数からスキーマ名を読み取る。- App 接続設定:
search_pathオプションを付与して接続する。 - カスタムスクリプト: マイグレーションやシード実行時にスキーマを明示的に指定する。
1. 設定ファイル (drizzle.config.ts)🔗
まず、DATABASE_URL から ?schema=xxx パラメータを解析し、Drizzle Kit に伝える設定を行います。
1import { defineConfig } from "drizzle-kit";2import "dotenv/config";34const connectionString = process.env.DATABASE_URL!;5const url = new URL(connectionString);6// クエリパラメータから schema を取得。なければ public7const searchPath = url.searchParams.get("schema") ?? "public";89export default defineConfig({10 dialect: "postgresql",11 schema: "./src/db/schema.ts",12 out: "./drizzle",13 dbCredentials: {14 url: connectionString,15 },16 migrations: {17 // マイグレーション管理テーブル(__drizzle_migrations)の作成先を指定18 schema: searchPath,19 },20});21
2. アプリケーションコードでの接続🔗
アプリケーションから DB に接続する際、PostgreSQL ドライバのオプションとして search_path を渡します。これにより、発行される SQL にスキーマ名が明記されていなくても、自動的に指定したスキーマがターゲットになります。
1import "dotenv/config";2import { drizzle } from "drizzle-orm/node-postgres";3import { relations } from "./db/relations.js";45const connectionString = process.env.DATABASE_URL;6if (!connectionString) throw new Error("DATABASE_URL is not set");78const url = new URL(connectionString);9const searchPath = url.searchParams.get("schema") ?? "public";1011const db = drizzle({12 connection: {13 connectionString,14 // セッションレベルで search_path を設定15 // これにより、以降のクエリはこのスキーマに対して実行される16 options: `--search_path=${searchPath}`,17 },18 relations,19});2021// 使用例22const main = async () => {23 const result = await db.query.posts.findMany({24 with: { author: true, categories: true }25 });26 console.log(result);27 db.$client.end();28};2930main();31
運用ツールの自作(Migrate, Seed, Reset)🔗
Drizzle Kit の CLI (drizzle-kit migrate 等)は便利ですが、動的なスキーマ切り替えや、環境ごとのクリーンアップを行うには柔軟性が足りない場合があります。そこで、Node.js スクリプトとして各コマンドを実装します。
これらのスクリプトは tools/ ディレクトリに配置し、drizzle.config.ts の設定を読み込んで動作させます。
A. マイグレーション実行 (tools/migrate.ts)🔗
CLI の代わりに drizzle-orm の migrate 関数を使用します。ここで重要なのが migrationsSchema オプションです。これを指定することで、マイグレーション履歴の管理テーブルも指定スキーマ内に作成されます。
1import config from "../drizzle.config";2import { drizzle } from "drizzle-orm/node-postgres";3import { migrate } from "drizzle-orm/node-postgres/migrator";45const main = async () => {6 // ... (接続チェック省略) ...78 const searchPath = config.migrations?.schema ?? "public";910 const db = drizzle({11 connection: {12 connectionString: config.dbCredentials.url,13 options: `--search_path=${searchPath}`,14 },15 });1617 if (!config.out) throw new Error("out is not set");1819 // スキーマを指定してマイグレーションを実行20 await migrate(db, {21 migrationsFolder: config.out,22 migrationsSchema: searchPath,23 });2425 await db.$client.end();26};2728main();29
B. データベースリセット (tools/reset.ts)🔗
開発中、DB を完全にクリーンな状態に戻したい場合があります。drizzle-kit にはこの機能が不足しているため、指定されたスキーマごと削除(DROP SCHEMA ... CASCADE)するスクリプトを用意します。
1// ... (imports) ...23const main = async () => {4 // ... (config読み込み) ...5 const searchPath = config.migrations?.schema ?? "public";67 const db = drizzle({8 connection: {9 connectionString: config.dbCredentials.url,10 options: `--search_path=${searchPath}`,11 },12 });1314 // スキーマごと削除してカスケード15 await db.execute(`DROP SCHEMA IF EXISTS "${searchPath}" CASCADE`);1617 // スキーマを再作成(必要に応じて)18 await db.execute(`CREATE SCHEMA IF NOT EXISTS "${searchPath}"`);1920 await db.$client.end();21};2223main();24
C. シーディング (tools/seed.ts)🔗
drizzle-seed を使用しますが、ここにも注意点があります。標準の reset 機能を使うと、スキーマ名が正しく扱われず public などを誤って参照してしまう挙動(バグまたは仕様)が見られました。
そのため、手動で TRUNCATE を行ってからデータを投入します。
1import config from "../drizzle.config";2import { drizzle } from "drizzle-orm/node-postgres";3import { seed } from "drizzle-seed";4import { sql, isTable } from "drizzle-orm";5import { getTableConfig, type PgTable } from "drizzle-orm/pg-core";6import * as schema from "../src/db/schema"; // スキーマ定義をインポート78const main = async () => {9 // ... (config読み込み) ...10 const searchPath = config.migrations?.schema ?? "public";1112 const db = drizzle({13 connection: {14 connectionString: config.dbCredentials.url,15 options: `--search_path=${searchPath}`,16 },17 });1819 await db.transaction(async (tx) => {20 // 全テーブルを動的に取得して TRUNCATE する21 const tables = Object.values(schema)22 .filter((t) => isTable(t))23 .map((t) => `"${getTableConfig(t as PgTable).name}"`)24 .join(", ");2526 if (tables.length > 0) {27 await tx.execute(sql.raw(`TRUNCATE TABLE ${tables} CASCADE;`));28 }2930 // データの投入31 await seed(tx, schema);32 });3334 await db.$client.end();35};3637main();38
実行方法🔗
package.json に以下のようにスクリプトを定義しておくとスムーズに運用できます。
1{2 "scripts": {3 "dev": "tsx src/index.ts",4 "docker": "docker compose up -d",5 "generate": "drizzle-kit generate",6 "migrate": "tsx tools/migrate.ts",7 "seed": "tsx tools/seed.ts",8 "reset": "tsx tools/reset.ts"9 }10}
ワークフロー🔗
- .env の設定: URL にターゲットスキーマを指定します。
1DATABASE_URL=postgres://user:pass@localhost:5432/db?schema=test_schema2
- DB 起動:
pnpm run docker - マイグレーション生成:
pnpm run generate(スキーマ変更時) - 適用 & データ投入:
1pnpm run migrate # 指定したスキーマにテーブル作成2pnpm run seed # 指定したスキーマにデータ投入3
- アプリ実行:
pnpm run dev
これで、test_schema 配下にデータが作られ、アプリもそこを参照して動くようになります。環境変数を書き換えるだけで、コードを変更することなく接続先スキーマをスイッチ可能です。
まとめ🔗
Drizzle ORM で動的なスキーマ切り替えを実現するには、標準の pgSchema 定義ではなく、以下の組み合わせが有効です。
- PostgreSQL 標準の
search_pathを接続オプションで利用する。 drizzle-kitの CLI に頼らず、migrateやseedをプログラム(スクリプト)から実行し、その際にスキーマ情報を注入する。
このアプローチにより、Prisma のような手軽さで、開発・テスト・本番環境のスキーマ分離を Drizzle でも実現できます。ぜひ試してみてください。