Cloudflare D1で業務システムのデータベースを設計する

Alcogyでは業務システムのOEM開発においてCloudflare D1を標準のデータストアとして採用しています。D1はSQLiteをベースにしたサーバーレスDBで、Workersと同一エッジで動くレイテンシの低さが最大の魅力です。一方で、MySQLやPostgreSQLでの設計に慣れていると、SQLiteならではの制約に何度か引っかかることになります。ここでは実際の業務システム開発で気をつけているポイントを整理します。

SQLiteは型を厳密にチェックしない

SQLiteには「型親和性(type affinity)」という仕組みがあり、カラムにINTEGERを指定していても文字列を挿入できてしまいます。アプリケーション側でバリデーションをかけずにいると、想定外の型のデータが紛れ込むことがあります。Alcogyでは後述するDrizzle ORMのスキーマ定義とZodによるバリデーションを組み合わせ、DB層に頼らずアプリケーション層で型を保証する方針を取っています。

Drizzle ORMでスキーマとマイグレーションを一元管理

D1のマイグレーションはWrangler CLIで管理できますが、生SQLだけで運用すると変更履歴が追いづらくなります。AlcogyではDrizzle ORMでスキーマをTypeScriptとして定義し、そこからマイグレーションSQLを生成する運用にしています。

// db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const orders = sqliteTable('orders', {
  id: text('id').primaryKey(),
  customerId: text('customer_id').notNull(),
  status: text('status', { enum: ['draft', 'confirmed', 'shipped'] })
    .notNull()
    .default('draft'),
  amount: integer('amount').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull()
    .default(new Date() as unknown as Date),
});
# スキーマからマイグレーションファイルを生成
bunx drizzle-kit generate

# ローカルD1に適用
bunx wrangler d1 migrations apply DB --local

# 本番D1に適用
bunx wrangler d1 migrations apply DB --remote

スキーマがコードとして残るため、レビュー時に差分が追いやすく、業務システムのように仕様変更が頻発するプロジェクトでは特に効いてきます。

外部キー制約はD1が標準で有効にしている

素のSQLite(better-sqlite3など)を使う場合、外部キー制約はデフォルトで無効になっており、接続のたびにPRAGMA foreign_keys = ONを発行しないと参照整合性がチェックされません。ここでハマった経験がある人も多いはずですが、D1に関してはその心配は不要です。D1はランタイム側で外部キー制約の適用がデフォルトで有効になっており、アプリケーション側で明示的にPRAGMAを発行しなくても、Drizzleのスキーマに書いたreferences()ON DELETE cascade / SET NULLなど)はそのまま効きます。「SQLiteだから」と身構えて余計な初期化コードを書き足す必要はなく、素直にD1のデフォルト挙動に任せてよい部分です。

集計クエリはD1の外に逃がすことも検討する

D1は1リクエストあたりの実行時間やクエリの複雑さに制約があり、大量データの複雑な集計には向いていません。Alcogyでは日次バッチでD1から集計用のサマリテーブルを作る、あるいはR2にエクスポートして分析するといった形で、トランザクション処理とレポーティングの役割を分離するようにしています。業務システムの初期フェーズでは気にならなくても、データが増えてきたタイミングで効いてくる部分なので、設計段階から意識しておくことをおすすめします。

D1は制約さえ理解していれば、業務システムのバックエンドとして十分実用的なDBです。SQLiteの特性を前提にした設計をすることが、後々のトラブルを避ける一番の近道だと感じています。