2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Cloudflare Workers + Supabase 環境でのDB関連技術選定の変遷

Last updated at Posted at 2025-07-24

この記事の目的

Cloudflare WorkersとSupabaseを採用する際に、特にデータベース関連の技術選定(ORM、マイグレーション、スキーマ管理など)で迷った経験はありませんか?本記事では、筆者が実際に直面した課題と試行錯誤の過程を赤裸々に綴ります。
この記事が、同じような構成で開発を進める方々にとって、未来の自分からの「欲しかった情報」となることを目指します。
2025/08/11 phase7追記
dbclient->driverに文言修正


目次

  1. この記事の目的
  2. 技術選定の要素と構造
  3. 固定条件
  4. 技術選定の変遷
  5. 今後の構想
  6. もし今から始めるなら:Neon DBで構築する理想構成
  7. まとめ: 技術選定の教訓

技術選定の要素と構造

技術選定の主要要素

現代のWebアプリケーション開発では、以下の要素が相互に影響し合いながら技術スタックを形成します:

Application Layer
      ↓
ORM Layer (型安全性・クエリビルダ)
      ↓
Driver Layer (接続・通信プロトコル)
      ↓
Database Layer (データ永続化)

本プロジェクトで検討した要素

  • Driver: データベース接続方式(TCP vs HTTP, プロトコル選択)
  • ORM: オブジェクト関係マッピング(型安全性、開発体験)
  • Migration: スキーマ変更管理(バージョニング、チーム協調)
  • Schema管理: スキーマ定義の真実の源泉(Code-first vs DB-first)
  • Seed: 初期データ・テストデータ管理
  • Transaction: トランザクション制御方式(App Layer vs DB Layer)

固定条件

Cloudflare Workers

  • 理由: 第一線で活躍するエンジニアからの推薦による。エッジコンピューティングが実現するパフォーマンスと、極めて快適な開発体験に魅力を感じたため。

Supabase (PostgreSQL)

  • 理由: 当時の技術トレンドであったことに加え、MySQLからPostgreSQLへ移行したいという個人的な関心があったため。

技術選定の変遷

Phase 1: Supabase オールイン戦略

初期構想: Supabase ですべてを解決

// 理想的だった初期構想
import { createClient } from '@supabase/supabase-js';

const supabase = createClient(url, key);

// シンプルなCRUD
const { data } = await supabase.from('users').select('*');
要素 選択 理由
Driver @supabase/supabase-js プラットフォーム統合
ORM なし(生API) Supabase API で十分
Migration Supabase CLI Supabaseオール・イン
Schema管理 Supabase CLI Supabaseオール・イン
Seed なし 初期段階では不要
Transaction なし 特別な技術が必要だとは想定していなかった

📝 振り返り・学び

単純に技術選定のための知識、特にトランザクションに関する理解が不足していた。


Phase 2: 宣言的スキーマ管理の必要性

💭 当時の状況・思考

この時点では、宣言的にスキーマを管理するという手法を知らなかった。
また、ベンダーロックインを懸念し、DBを切り離して汎用的に管理したいという意図があった。

※ 宣言的スキーマ管理とは
宣言的: コードでスキーマの「あるべき姿」を定義し、ツールが現状との差分を自動計算・適用
Migration管理: 変更を順次SQLファイルで記録し、履歴順に適用
例: Prisma schema.prisma → 自動でALTER文生成 vs 手動で 001_add_users.sql 作成

※ ベンダーロックインとは
特定のクラウドサービスに依存しすぎて、他のサービスへの移行が困難になること
例: Supabaseダッシュボードでスキーマ管理 → 移行時にSupabase固有設定の再構築が必要

❌ 問題発生

ベンダーロックインとスキーマ管理

✅ 解決策・決断

Prisma でスキーマ管理

// prisma/schema.prisma
model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}
要素 変更前 変更後 理由
Schema管理 Supabase CLI Prisma Schema 宣言的管理、VC対応
ORM 生API 検討中 型安全性向上

📝 振り返り・学び

宣言的管理という概念は以前から漠然と認識していたが、この時初めて深く調査し、その後の開発に大きな影響を与えた。この宣言的な考え方から発想して、Seedデータも決定論的に管理するアプローチを採用した。

※ 決定論的Seedデータ管理
UUIDを決定論的に生成 → 何度実行しても同一データ
例: user_id: uuid("admin-user") → 常に同じUUIDが生成され、全環境で一致


Phase 3: 権限管理とマイグレーションの複雑化

❌ 問題発生

Prisma では DB 権限やfunctionを宣言的に管理できない

Prismaでpushしたスキーマに対し、APIからアクセスを試みたところ、権限不足でテーブルにアクセスできない問題が発生した。Prismaのスキーマ定義はテーブル構造のみに特化しており、データベース権限(GRANT/REVOKE)やPostgreSQL関数を宣言的に管理することができない※。やむを得ず、Supabase CLIによるマイグレーションを導入するに至った。

※SupabaseのAPI経由でアクセスする場合、public schemaへのGRANT権限が必要だった

✅ 解決策・決断

Supabase CLI でマイグレーション導入

この時点でreset-dbコマンドはかなり複雑化した。この状況を改善したいと考えつつも、当時は開発の進行を優先した。

-- supabase/migrations/xxx_add_rls.sql
ALTER TABLE users ENABLE ROW LEVEL SECURITY;

CREATE POLICY "users_select_own" ON users
FOR SELECT USING (auth.uid() = id);
要素 Phase2 Phase3 理由
Migration Prisma Migrate Supabase CLI 権限管理対応
Schema管理 Prisma Schema Prisma + SQL ハイブリッド管理

📝 振り返り・学び

プロトタイプの開発を優先するあまり、この時点での技術選定を深く検討せず、力技で開発を進めてしまった。最終的にこの構成は変更することになったが、時には実装を優先すべき局面もあり、まさにここがそのタイミングだったと今では考えている。


Phase 4: トランザクション制御の限界

💭 当時の状況・思考

購入関連機能の実装を進める中で、Supabaseのクライアントライブラリでは、複数のDB操作を一つのトランザクションとしてまとめられないという制約に直面しました。

SupabaseのクライアントはHTTPベースで動作するため、以下のように記述したコードはそれぞれが独立したリクエストとして実行されます。そのため、途中の処理でエラーが発生しても、それ以前の操作をロールバック(取り消し)できません。

❌ 問題発生

HTTP ベースでアプリケーション層トランザクション不可

// 各APIリクエストが独立しているため、アトミック性が保証されない
// usersテーブルへの挿入
await supabase.from('users').insert(userData);
// もし次の処理でエラーが発生しても、↑のユーザー作成は取り消されない
await supabase.from('orders').insert(orderData);

❌ Supabase公式の推奨策とその課題

Supabaseでは、このような一連の処理を**DB Function(ストアドプロシージャ)としてデータベース層に定義し、それをクライアントから呼び出す方法を推奨しています。この方法であれば、一連の処理の原子性(アトミック性)**が保証されます。

※ アトミック性
関連する一連の処理が**「すべて成功」するか「すべて失敗(実行前の状態に戻る)」するかのどちらか**であることが保証される性質。

しかし、このアプローチを試みた結果、新たな課題が浮上しました。

  • 複雑化するロジック管理

    • 複雑なビジネスロジックをSQLで記述する必要があり、可読性やメンテナンス性が低下する。
    • デバッグが困難になる。
  • 開発プロセスの非効率化

    • 機能が増えるたびにDB Functionが増加し、マイグレーションファイルでのバージョン管理が非常に煩雑になる。

🔍 試行錯誤・調査

当初はSupabaseの思想に沿って開発を進めていましたが、DB Functionの管理コストが現実的ではないと判断し、他の方法を探し始めました。宣言的に管理できるツールとして「Atlas」も候補に挙がりましたが、npmパッケージではなかったため採用には至りませんでした。

振り返りと学び

今回の経験から、SupabaseのDB Functionによるトランザクション管理は、以下のようなケースでは有効な選択肢だと考えられます。

  • 小規模なアプリケーションで、トランザクション処理が少ない場合
  • 処理ロジックが単純で、変更頻度が低い場合

一方で、複雑なビジネスロジックや頻繁な仕様変更が求められるプロジェクトでは、DB Functionに依存しすぎると管理コストが膨らむ可能性があります。プロジェクトの特性に応じて、アーキテクチャを慎重に選択することの重要性を学びました。


Phase 5: Driver とORMの選択

💭 当時の状況・思考

この段階に至り、初めてDriverとORMの選定に本格的に着手した。

🔍 試行錯誤・調査

検討した選択肢:

  1. Prisma Client: 学習コストがなく理想的だが、Cloudflare Workersに非対応。クエリも独特。
  2. Drizzle ORM: Workersに対応しており、型安全性も高い。クエリがSQLに近く、馴染みやすかった。

❌ Prisma Client の制約

// ❌ Cloudflare Workers で実行不可
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient(); // バイナリエンジン依存

✅ Drizzle ORM の採用

// ✅ Workers で動作
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';

const client = postgres(connectionString);
const db = drizzle(client);

// アプリケーション層トランザクション
await db.transaction(async (tx) => {
  const [user] = await tx.insert(users).values(userData).returning();
  await tx.insert(orders).values({...orderData, userId: user.id});
});
要素 Phase3 Phase4 理由
Driver Supabase Client postgres.js TCP直接接続、Workers対応
ORM なし Drizzle 型安全性、Workers対応
Transaction HTTP制限 App Layer 柔軟な制御

Phase 6: ツール乱立による複雑化と、理想への葛藤

ツール乱立による複雑化:

  • Prisma: スキーマ定義・開発時DB管理、Seedデータ挿入
  • Supabase CLI: マイグレーション・権限管理
  • Drizzle: 本番ORM・型生成
  • postgres.js: 接続クライアント

現在の開発フロー例:(コマンドで統一済)

# DBスキーマ変更時・dbリセット
# 1. schema.prisma変更
# 2. Supabaseリセット
# 3. Prismaスキーマプッシュ
# 4. ローカルマイグレーション削除
# 5. Supabaseマイグレーション適用
# 6. seedデータ挿入
# 7. drizzle型生成

理想: Drizzle 統一

  # DBスキーマ変更時
  # 1. schema.ts (Drizzle) を編集
  # 2. drizzle-kit generate     # 2. マイグレーション生成
  # 3. drizzle-kit migrate      # 3. マイグレーション適用 + seed実行

❌ 現実的な制約: Prisma の開発体験が良すぎる

Prismaによるスキーマ管理の体験は極めて優れている。schema.prismaはコード量が少なく直感的であり、AI(LLM)にコンテキストとして与える際のトークン量削減にも寄与する。開発期間中はこの利点を最大限に活用したいという思いがあった。

// Prisma の直感的なスキーマ定義
model User {
  id      String   @id @default(cuid())
  email   String   @unique
  posts   Post[]   // リレーション定義が簡潔
  profile Profile? @relation(fields: [profileId], references: [id])
}

現在の妥協案(Phase 6時点)

要素 開発時 本番時 移行予定
Schema管理 Prisma Schema Drizzle型生成 Drizzle Schema
Migration Prisma Migrate Drizzle Kit Drizzle Kit
ORM - Drizzle Drizzle
Driver - postgres.js postgres.js
Seed Prisma Script - Drizzle Script

Phase 7: 開発環境のリセットフロー簡略化

2025/08/11 追記

💭 当時の状況・思考

Phase 6まで到達した時点で、開発時のDBリセットコマンドが非常に複雑になっていた。特に問題だったのは、dev環境でSupabaseのauthスキーマに対する権限エラーが頻発し、回避するためにsupabase reset → prisma push → migration削除 → migration upという複雑な手順を踏む必要があった点だ。

❌ 問題発生

Supabase resetによるauth権限エラー

# 以前の複雑なreset-db:prod
supabase reset:prod → prisma push → migration削除 → migration up → seed
# ↑ authスキーマをリセットしようとして権限エラー

✅ 解決策・決断

authスキーマには触らない戦略

問題の本質を分析した結果、以下のことが判明した:

  • 構造変更(DDL): Supabaseが管理するauthスキーマのテーブル構造変更は権限エラー
  • データ操作(DML): service_roleでauth.usersのレコード操作は問題なし

この理解に基づき、supabase resetを完全に削除し、Prismaとdrizzleで必要な処理のみを行うように変更した。

-- drizzle/functions/0000_skip_auth_schema.sql
-- authスキーマの構造は触らず、データのみクリア
DELETE FROM auth.users; -- 開発環境のみ、環境変数で制御

🔍 Drizzleの疑似マイグレーション活用

さらに重要な発見があった。当初、Drizzleへの完全移行は本番運用開始まで不可能だと考えていた。なぜなら:

  • Prismaでスキーマ管理している限り、Drizzle migrationは使えない
  • schema.tsが存在しないため、正式なマイグレーション生成ができない

しかし、Drizzleを疑似マイグレーションツールとして活用できることに気づいた:

// apply-functions.ts
// SQLファイルを順番に実行するシンプルな仕組み
const files = readdirSync('functions').filter(f => f.endsWith('.sql')).sort();
for (const file of files) {
  await sql.unsafe(readFileSync(file, 'utf-8'));
}

この方法により、実質的にDrizzleでほぼ全ての処理を管理できるようになった:

  • 関数・トリガー定義: 0001_auth_hook.sqlなど
  • 権限設定: 0003_granted_service_role.sql
  • auth.usersクリア: 0000_skip_auth_schema.sql(環境変数制御)
要素 Phase6 Phase7 理由
リセット処理 supabase reset + 複雑な手順 prisma push + drizzle apply authスキーマ回避
auth.users管理 不明確 環境変数制御 本番安全性確保
Migration役割 Supabase CLI Drizzle(疑似) 実質的な統一
SQL管理 分散(Supabase/Prisma) Drizzle functions/に集約 管理の一元化

📝 振り返り・学び

この改善により、開発時のDBリセットが大幅に簡略化された。重要な学びは:

  • 権限エラーの本質を理解することで、適切な回避策を見つけられた
  • 不要な処理を削除(supabase reset)することで、処理時間も短縮
  • 環境変数による制御で、開発と本番の挙動を安全に分離
  • 疑似マイグレーションという発想で、完全移行前でもDrizzleに実質統一

開発中はデータを入れ直すことを前提とした割り切りと、ツールの柔軟な活用により、Phase 6で悩んでいた「ツール乱立」問題が大幅に改善された。本番運用時の正式なDrizzle migration移行への道筋も明確になった。


今後の構想

本番運用に向けた移行計画

  1. Phase 8: Prisma Schema → Drizzle Schema 変換
  2. Phase 9: Drizzle Kit での完全なマイグレーション管理

もし今から始めるなら:Neon DBで構築する理想構成

※これはあくまで仮定の話で実際にneonを使用していないものの想像です

理想的な技術選定プロセス

なぜNeon DBか:

Neon DBを選択する最大の理由は、現状のプロジェクトでSupabaseの多機能性を活かしきれていない点にある。Storage、Supabase CLI、Edge Functionは使用していないし、Authは利用しているものの、他のツールで十二分に代替可能であるように思う。一方、Neonが提供する無料のDBブランチ機能やサーバーレス環境での自動スケーリングは、本プロジェクトにとって非常に魅力的に見える。

段階的な理想構成:

開発期間中(本番運用まで):

// Prisma中心の開発フロー(db-reset前提)
// schema.prisma → DB → Drizzle introspect → schema.ts

// 1. Prismaでスキーマ定義(開発効率優先)
// prisma/schema.prisma
model User {
  id    String @id @default(cuid())
  email String @unique
}

// 2. 本番実行はDrizzle(Workers対応)
import { drizzle } from 'drizzle-orm/neon-http';
const db = drizzle(neon(connectionString));

注意: 開発期間中はdb-reset前提のため、Prisma push後の他ツールマイグレーション実行は問題なし。履歴管理不要で任意のツール組み合わせが可能。

本番運用開始時:

// Drizzle Schema中心に移行
// drizzle schema.ts → マイグレーション → DB

export const users = pgTable('users', {
  id: text('id').primaryKey(),
  email: text('email').unique().notNull()
});

// 統一されたフロー
// drizzle-kit generate → drizzle-kit migrate
要素 開発期間中 本番運用時 理由
DB Neon DB Neon DB Serverless最適化、ブランチング
Driver @neondatabase/serverless @neondatabase/serverless HTTP、Workers完全対応
ORM Drizzle Drizzle 軽量、型安全、Workers対応
Migration Prisma Migrate Drizzle Kit 開発効率 → 一貫性
Schema Prisma Schema → introspect Drizzle Schema AI開発用 → 実行用
Seed Prisma Script Drizzle Script 開発効率 → 環境一貫性

移行戦略の利点:

  • 開発期間: Prismaの優れた開発体験を活用
  • 本番運用: Drizzleの統一性とパフォーマンスを享受
  • 段階的移行: リスクを最小化しながら最適化

Schema管理のハイブリッド戦略:

// schema.ts (Drizzle) - 実行用
export const users = pgTable('users', {
  id: uuid('id').primaryKey(),
  email: text('email').unique().notNull(),
  name: text('name')
});

// schema.prisma (Introspect) - AI開発用
// npx prisma db pull でDrizzleで作成したDBから自動生成
model User {
  id    String @id @db.Uuid
  email String @unique
  name  String?
}

AI適正の観点:

  • Drizzle Schema: 型安全な実行・マイグレーション用
  • Prisma Schema: 1ファイルで全体構造把握、AI開発時の参照用
  • 最高の開発体験: 実行効率 + AI可読性の両立

避けるべき選択

  1. 段階的移行: 最初から最終形を目指す
  2. 複数ツール混在: 開発・本番環境の分離を避ける
  3. 過度な最適化: 早すぎる最適化より一貫性重視

まとめ: 技術選定の教訓

重要な原則

  1. 基盤システムから設計する: トランザクション制御のような基盤技術は、後から変更すると全体のアーキテクチャに大きな影響を与える。UI開発に入る前に、データ操作の基本パターンを明確にし、それに適したツール選定を行う。

  2. 最低限の移行余地を残す: DB層の分離(例:Drizzle接続の抽象化)など、過度ではない最小限の設計で将来の変更に対応する。

  3. 複数ツールの試行錯誤を恐れない: 様々なツールを試すことで各ツールの特性や制約を深く理解でき、最終的により適切な選択と効率的な実装につながる。

  4. フェーズに応じた最適化: 開発期間中は開発効率を、本番運用時はパフォーマンスと一貫性を重視するなど、プロジェクトのフェーズに応じて優先度を調整する。

避けるべき落とし穴

  • 最初から完璧を求める: 要件が変化する前提で柔軟な設計を心がける
  • 単一ツール至上主義: ツールの制約に振り回されるより、要件に最適な組み合わせを選ぶ
  • ツール統一の強制: 開発フェーズを無視した一貫性の追求は非効率

この変遷の記録が、Cloudflare WorkersとサーバーレスDBという同様の環境で開発を進める方々の参考となれば幸いです。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?