概要
この記事では、JavaScriptやTypeScriptのプロジェクトで使用できる軽量ORM「Drizzle ORM」を用いて、Cloudflareのサーバレスデータベース「Cloudflare D1」に接続する方法を詳しく解説します。Drizzle ORMは、型安全性が高く、直感的なSQLスタイルのクエリ構文で操作できるため、データベース操作を効率的かつ安全に行えます。Cloudflare D1はSQLiteベースのサーバレスデータベースで、特にスケーラビリティが求められるWebアプリケーションに適しています。
Drizzle ORMとは
Drizzle ORMは、JavaScriptとTypeScriptでのデータベース操作を簡単にし、型安全なクエリを可能にするORM(オブジェクトリレーショナルマッピング)ツールです。特徴は以下の通りです。
- TypeScriptとの相性:型安全な設計により、データベース操作のエラーを事前に防ぎます。
- 簡単なクエリ構文:SQLに似た構文で、データ操作がしやすい。
- 効率的で軽量:高速で軽量な設計のため、パフォーマンスも良好です。
Cloudflare D1とは
Cloudflare D1は、Cloudflareのインフラ上で動作するSQLiteベースのサーバレスデータベースです。特徴は以下の通りです。
- サーバレスでスケーラブル:インフラ管理不要で自動的にスケールします。
- SQLiteベース:軽量なため、高速かつシンプルに操作可能。
- Cloudflare Workersと統合可能:フロントエンドから直接アクセスでき、応答速度が早い。
Cloudflare D1と
DrizzleORMを組み合わせることで、スケーラブルでメンテナンスが容易なデータベース管理が可能になります。
Drizzle ORMを用いたCloudflare D1への接続手順
プロジェクトのセットアップ
Drizzle ORMとCloudflare D1を利用するには、まずNode.js環境でプロジェクトを準備します。
# プロジェクトのディレクトリを作成
mkdir drizzle-cloudflare-d1
cd drizzle-cloudflare-d1
# npmプロジェクトの初期化
npm init -y
必要なパッケージのインストール
Cloudflare D1との接続を可能にするため、必要なパッケージをインストールします。
# Drizzle ORM とD1用のドライバをインストール
npm install drizzle-orm
# Cloudflare Workers 開発ツール(Wrangler)をインストール
npm install -D wrangler
# Drizzle Kit(マイグレーションツール)をインストール
npm install -D drizzle-kit
drizzle-orm: Drizzle ORM本体(D1用のドライバも含む)wrangler: Cloudflare Workersの開発・デプロイツールdrizzle-kit: スキーマからマイグレーションを生成するツール
Cloudflare D1データベースの作成と設定
Cloudflare D1を使用するには、Cloudflareアカウントが必要です。Wranglerコマンドを使用してデータベースを作成します。
# Cloudflareにログイン
npx wrangler login
# D1データベースを作成
npx wrangler d1 create my-database
# 出力例:
# ✅ Successfully created DB 'my-database'
#
# [[d1_databases]]
# binding = "DB"
# database_name = "my-database"
# database_id = "xxxx-xxxx-xxxx-xxxx"
出力された設定情報を wrangler.toml ファイルに追加します。
# wrangler.toml
name = "my-worker"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB" # Workerコード内で使用する変数名
database_name = "my-database"
database_id = "xxxx-xxxx-xxxx-xxxx" # 実際のデータベースID
この設定により、Workerコード内で env.DB としてD1データベースにアクセスできます。
Drizzle ORMとCloudflare D1の接続設定
Cloudflare Workers内でDrizzle ORMを使用してD1に接続します。Workersでは env オブジェクトから D1 インスタンスにアクセスできます。
// src/db/index.ts
import { drizzle } from 'drizzle-orm/d1';
// Cloudflare Workers の環境型定義
export interface Env {
DB: D1Database; // wrangler.toml で定義した binding 名
}
// Drizzle ORM インスタンスを作成する関数
export function getDb(env: Env) {
return drizzle(env.DB);
}
Cloudflare Workers内でD1を使用する際は、Workers環境からD1インスタンス (env.DB) を受け取り、それをDrizzle ORMでラップして使用します。
テーブルのスキーマ定義
Drizzle ORMでは、データベーステーブルのスキーマをTypeScriptの型として定義します。以下はtasksテーブルを定義する例です。
// src/db/schema.ts
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
// tasks テーブルの定義
export const tasks = sqliteTable('tasks', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
description: text('description'),
completed: integer('completed', { mode: 'boolean' }).notNull().default(false),
priority: integer('priority').notNull().default(0),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.default(sql`(unixepoch())`),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.default(sql`(unixepoch())`)
});
// 型定義の自動生成
export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;
このスキーマ定義では、以下の機能を実装しています:
integer().primaryKey({ autoIncrement: true }): 自動インクリメントの主キーtext(): 文字列型(SQLiteでは TEXT 型)integer({ mode: 'boolean' }): 真偽値(SQLiteでは 0/1 として保存)integer({ mode: 'timestamp' }): タイムスタンプ(UNIXエポック秒)sql``: SQLite関数の直接使用(デフォルト値にunixepoch()` 関数)- 型推論による型安全性(
$inferSelect,$inferInsert)
基本的なデータ操作 - 挿入、取得、更新、削除
Drizzle ORMを使用して、Cloudflare D1上のテーブルに対して基本的なデータ操作を行います。
データの挿入
import { drizzle } from 'drizzle-orm/d1';
import { tasks, type NewTask } from './schema';
import { eq } from 'drizzle-orm';
// 単一タスクの挿入
async function insertTask(db: ReturnType<typeof drizzle>, data: NewTask) {
const result = await db.insert(tasks).values(data).returning();
return result[0];
}
// 使用例
const newTask = await insertTask(db, {
title: 'Drizzle ORMを学ぶ',
description: 'Cloudflare D1との連携方法を習得する',
priority: 1
});
// 複数タスクの一括挿入
async function insertMultipleTasks(db: ReturnType<typeof drizzle>, taskList: NewTask[]) {
return await db.insert(tasks).values(taskList).returning();
}
await insertMultipleTasks(db, [
{ title: 'タスク1', priority: 1 },
{ title: 'タスク2', priority: 2 },
{ title: 'タスク3', priority: 3 }
]);
データの取得
// 全タスク取得
async function getAllTasks(db: ReturnType<typeof drizzle>) {
return await db.select().from(tasks);
}
// 条件付き取得
async function getTaskById(db: ReturnType<typeof drizzle>, id: number) {
const result = await db
.select()
.from(tasks)
.where(eq(tasks.id, id))
.limit(1);
return result[0];
}
// 未完了タスクの取得(優先度順)
import { and, desc } from 'drizzle-orm';
async function getIncompleteTasks(db: ReturnType<typeof drizzle>) {
return await db
.select()
.from(tasks)
.where(eq(tasks.completed, false))
.orderBy(desc(tasks.priority), tasks.created_at);
}
// LIKE検索
import { like } from 'drizzle-orm';
async function searchTasks(db: ReturnType<typeof drizzle>, keyword: string) {
return await db
.select()
.from(tasks)
.where(like(tasks.title, `%${keyword}%`));
}
// ページネーション
async function getTasksWithPagination(
db: ReturnType<typeof drizzle>,
page: number = 1,
pageSize: number = 10
) {
const offset = (page - 1) * pageSize;
return await db
.select()
.from(tasks)
.limit(pageSize)
.offset(offset);
}
データの更新
// タスクの完了状態を更新
async function completeTask(db: ReturnType<typeof drizzle>, id: number) {
const result = await db
.update(tasks)
.set({
completed: true,
updated_at: new Date()
})
.where(eq(tasks.id, id))
.returning();
return result[0];
}
// 複数フィールドの更新
async function updateTask(
db: ReturnType<typeof drizzle>,
id: number,
data: Partial<NewTask>
) {
return await db
.update(tasks)
.set({
...data,
updated_at: new Date()
})
.where(eq(tasks.id, id))
.returning();
}
// 使用例
await updateTask(db, 1, {
title: '更新されたタスク',
priority: 5,
description: '新しい説明'
});
// 一括更新(全タスクの優先度を1上げる)
import { sql } from 'drizzle-orm';
async function increasePriority(db: ReturnType<typeof drizzle>) {
await db
.update(tasks)
.set({
priority: sql`${tasks.priority} + 1`,
updated_at: new Date()
});
}
データの削除
// 単一タスクの削除
async function deleteTask(db: ReturnType<typeof drizzle>, id: number) {
const result = await db
.delete(tasks)
.where(eq(tasks.id, id))
.returning();
return result[0];
}
// 完了済みタスクを全削除
async function deleteCompletedTasks(db: ReturnType<typeof drizzle>) {
return await db
.delete(tasks)
.where(eq(tasks.completed, true))
.returning();
}
// 古いタスクの削除(30日以上前)
import { lt } from 'drizzle-orm';
async function deleteOldTasks(db: ReturnType<typeof drizzle>, daysOld: number = 30) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
const cutoffTimestamp = Math.floor(cutoffDate.getTime() / 1000);
return await db
.delete(tasks)
.where(lt(tasks.created_at, new Date(cutoffTimestamp * 1000)))
.returning();
}
Cloudflare Workersでの実装例
Cloudflare D1をCloudflare Workersから操作する、RESTful APIの完全な実装例を示します。
// src/index.ts
import { drizzle } from 'drizzle-orm/d1';
import { tasks, type NewTask } from './db/schema';
import { eq, desc } from 'drizzle-orm';
export interface Env {
DB: D1Database;
}
// ルーターの実装
export default {
async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
const db = drizzle(env.DB);
const url = new URL(request.url);
const path = url.pathname;
// CORS ヘッダー
const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type',
'Content-Type': 'application/json'
};
// OPTIONSリクエスト(CORS preflight)
if (request.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
try {
// GET /tasks - 全タスク取得
if (path === '/tasks' && request.method === 'GET') {
const completed = url.searchParams.get('completed');
const search = url.searchParams.get('search');
let query = db.select().from(tasks);
// フィルタリング
if (completed !== null) {
query = query.where(eq(tasks.completed, completed === 'true'));
}
const result = await query.orderBy(desc(tasks.priority), tasks.created_at);
// 検索フィルタ(メモリ上で実行)
let filteredResult = result;
if (search) {
filteredResult = result.filter(task =>
task.title.toLowerCase().includes(search.toLowerCase())
);
}
return new Response(JSON.stringify({
success: true,
data: filteredResult,
count: filteredResult.length
}), {
headers: corsHeaders,
status: 200
});
}
// GET /tasks/:id - 特定タスク取得
const taskIdMatch = path.match(/^\/tasks\/(\d+)$/);
if (taskIdMatch && request.method === 'GET') {
const id = parseInt(taskIdMatch[1]);
const result = await db
.select()
.from(tasks)
.where(eq(tasks.id, id))
.limit(1);
if (result.length === 0) {
return new Response(JSON.stringify({
success: false,
error: 'Task not found'
}), {
headers: corsHeaders,
status: 404
});
}
return new Response(JSON.stringify({
success: true,
data: result[0]
}), {
headers: corsHeaders,
status: 200
});
}
// POST /tasks - 新規タスク作成
if (path === '/tasks' && request.method === 'POST') {
const body = await request.json() as NewTask;
// バリデーション
if (!body.title || body.title.trim() === '') {
return new Response(JSON.stringify({
success: false,
error: 'Title is required'
}), {
headers: corsHeaders,
status: 400
});
}
const result = await db
.insert(tasks)
.values({
title: body.title,
description: body.description || null,
priority: body.priority || 0,
completed: false
})
.returning();
return new Response(JSON.stringify({
success: true,
message: 'Task created successfully',
data: result[0]
}), {
headers: corsHeaders,
status: 201
});
}
// PUT /tasks/:id - タスク更新
if (taskIdMatch && request.method === 'PUT') {
const id = parseInt(taskIdMatch[1]);
const body = await request.json() as Partial<NewTask>;
const result = await db
.update(tasks)
.set({
...body,
updated_at: new Date()
})
.where(eq(tasks.id, id))
.returning();
if (result.length === 0) {
return new Response(JSON.stringify({
success: false,
error: 'Task not found'
}), {
headers: corsHeaders,
status: 404
});
}
return new Response(JSON.stringify({
success: true,
message: 'Task updated successfully',
data: result[0]
}), {
headers: corsHeaders,
status: 200
});
}
// DELETE /tasks/:id - タスク削除
if (taskIdMatch && request.method === 'DELETE') {
const id = parseInt(taskIdMatch[1]);
const result = await db
.delete(tasks)
.where(eq(tasks.id, id))
.returning();
if (result.length === 0) {
return new Response(JSON.stringify({
success: false,
error: 'Task not found'
}), {
headers: corsHeaders,
status: 404
});
}
return new Response(JSON.stringify({
success: true,
message: 'Task deleted successfully'
}), {
headers: corsHeaders,
status: 200
});
}
// 404 Not Found
return new Response(JSON.stringify({
success: false,
error: 'Not found'
}), {
headers: corsHeaders,
status: 404
});
} catch (error: any) {
console.error('Error:', error);
return new Response(JSON.stringify({
success: false,
error: error.message || 'Internal server error'
}), {
headers: corsHeaders,
status: 500
});
}
}
};
このWorkerは、完全なRESTful APIを実装しており、以下の機能を提供します:
- GET /tasks: 全タスク取得(フィルタリング・検索機能付き)
- GET /tasks/:id: 特定タスク取得
- POST /tasks: 新規タスク作成
- PUT /tasks/:id: タスク更新
- DELETE /tasks/:id: タスク削除
- CORS対応: ブラウザからのアクセスに対応
マイグレーションの実行
Drizzle Kitを使用してスキーマからマイグレーションSQLを生成し、D1に適用します。
// drizzle.config.ts
import type { Config } from 'drizzle-kit';
export default {
schema: './src/db/schema.ts',
out: './migrations',
driver: 'd1',
dbCredentials: {
wranglerConfigPath: './wrangler.toml',
dbName: 'my-database'
}
} satisfies Config;
マイグレーション実行手順:
# 1. スキーマからマイグレーションSQLを生成
npx drizzle-kit generate:sqlite
# 2. ローカルD1にマイグレーションを適用
npx wrangler d1 execute my-database --local --file=./migrations/0000_initial.sql
# 3. 本番D1にマイグレーションを適用
npx wrangler d1 execute my-database --remote --file=./migrations/0000_initial.sql
# 4. マイグレーション履歴の確認
npx wrangler d1 execute my-database --command "SELECT * FROM __drizzle_migrations"
デプロイ方法
# ローカルでの開発サーバー起動
npx wrangler dev
# 本番環境へのデプロイ
npx wrangler deploy
# デプロイ後のログ確認
npx wrangler tail
package.jsonのスクリプト設定
{
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy",
"generate": "drizzle-kit generate:sqlite",
"migrate:local": "wrangler d1 execute my-database --local --file=./migrations/0000_initial.sql",
"migrate:remote": "wrangler d1 execute my-database --remote --file=./migrations/0000_initial.sql"
}
}
エラー対処とヒント
1. D1バインディングエラー
問題: env.DB is undefined エラーが発生する
対処法:
wrangler.tomlのバインディング名が正しいか確認- ローカル開発時は
--localフラグを使用 - 型定義が正しく設定されているか確認
// 正しい型定義
export interface Env {
DB: D1Database; // wrangler.toml の binding 名と一致
}
2. マイグレーションエラー
問題: マイグレーション適用時にエラーが発生する
対処法:
# ローカルD1の状態を確認
npx wrangler d1 execute my-database --local --command "SELECT name FROM sqlite_master WHERE type='table'"
# D1をリセット(開発時のみ)
npx wrangler d1 execute my-database --local --command "DROP TABLE IF EXISTS tasks"
# マイグレーションを再適用
npx wrangler d1 execute my-database --local --file=./migrations/0000_initial.sql
3. SQLite固有の制限
注意点:
- SQLiteには
SERIAL型がない →INTEGER PRIMARY KEY AUTOINCREMENTを使用 - BOOLEAN型は整数(0/1)として保存される
- タイムスタンプはUNIXエポック秒で保存
- JOINの最適化が限定的
// SQLiteでの真偽値の扱い
const completed = integer('completed', { mode: 'boolean' }); // 0 or 1
// SQLiteでのタイムスタンプ
const created_at = integer('created_at', { mode: 'timestamp' })
.default(sql`(unixepoch())`);
4. D1の制限とベストプラクティス
Cloudflare D1の制限:
- データベースサイズ: 2GB(Freeプラン)
- 1日あたりのリクエスト数: 100,000回(Freeプラン)
- クエリ実行時間: 30秒まで
- トランザクションサイズ: 最大1MB
ベストプラクティス:
// 1. インデックスの活用
export const tasks = sqliteTable('tasks', {
// ...
}, (table) => ({
// 頻繁に検索するカラムにインデックスを作成
completedIdx: index('completed_idx').on(table.completed),
priorityIdx: index('priority_idx').on(table.priority)
}));
// 2. バッチ処理
async function batchInsert(db: ReturnType<typeof drizzle>, items: NewTask[]) {
const batchSize = 100;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await db.insert(tasks).values(batch);
}
}
// 3. エラーハンドリング
try {
await db.insert(tasks).values({ title: 'Test' });
} catch (error: any) {
if (error.message?.includes('UNIQUE constraint')) {
// ユニーク制約違反の処理
console.error('Duplicate entry');
} else {
throw error;
}
}
5. ローカル開発のヒント
# ローカルD1の永続化ディレクトリを確認
# .wrangler/state/v3/d1/miniflare-D1DatabaseObject/{database-id}.sqlite
# SQLiteブラウザでローカルDBを直接確認
# https://sqlitebrowser.org/
# Wranglerのログレベルを上げる
npx wrangler dev --log-level debug
6. 型安全性の確保
// Zodスキーマでリクエストバリデーション
import { z } from 'zod';
const taskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(1000).optional(),
priority: z.number().int().min(0).max(10).optional(),
completed: z.boolean().optional()
});
// Workerでの使用
const body = taskSchema.parse(await request.json());
まとめ
Drizzle ORMを用いてCloudflare D1に接続することで、型安全なデータ操作が可能です。Cloudflare D1は軽量なSQLiteをベースとしており、スケーラビリティとパフォーマンスに優れています。さらに、Drizzle ORMの型安全なクエリ構文によって、データベース操作の信頼性が向上します。サーバレス環境で効率的にデータを管理したい場合に、Drizzle ORMとCloudflare D1の組み合わせは非常に有用です。
主な特徴:
- 型安全性: TypeScriptの完全な型推論により、開発時にエラーを検出
- エッジコンピューティング: Cloudflareのグローバルネットワークで高速レスポンス
- SQLiteベース: 軽量で高速なSQLite互換データベース
- サーバレス: インフラ管理不要で自動スケーリング
- 無料枠: 開発・小規模プロジェクトに十分な無料プランを提供
- Wrangler統合: CLIツールによる簡単なデプロイとマイグレーション
開発のポイント:
- スキーマ定義はTypeScriptで型安全に記述
- Drizzle Kitでマイグレーションを自動生成
- ローカル開発は
wrangler dev --localで実施 - 本番デプロイ前に必ずマイグレーションを適用
- D1の制限(サイズ、リクエスト数)を考慮した設計