cloudflare-d1 について
cloudflare-d1 は、Workers から Cloudflare D1 を利用する開発者向けの実践ガイドです。データベース設定、マイグレーション、クエリ、ローカル開発を Wrangler の例とともに扱います。
主な機能
- 01データベースとバインディングの設定
- 02バージョン管理された SQL マイグレーション
- 03プリペアドステートメントとバッチクエリ
- 04ローカルおよびリモートのデータベース運用
- 05クエリ性能とエラー対応のガイド
活用シーン
- 01D1 データベースの作成とマイグレーション
- 02Worker からの D1 クエリ
- 03関連するデータベース操作の一括処理
- 04スキーマ変更のローカルテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ ovachiever/ droid-tings --skill cloudflare-d1インストール後、Claude Code で/cloudflare-d1を入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
README.md.claude-plugin/plugin.jsonreferences/best-practices.mdquery-patterns.mdtemplates/d1-setup-migration.shd1-worker-queries.tsschema-example.sql詳細
Cloudflare D1 Database
ステータス:本番環境で利用可能 ✅ 最終更新日:2025-11-23 依存関係:cloudflare-worker-base(Worker のセットアップ用) 最新バージョン:[email protected]、@cloudflare/[email protected]
最近の更新(2025年):
- 2025年11月:管轄区域のサポート(データローカライゼーションへの準拠)、リモートバインディングが GA に([email protected] 以降)、リソースの自動プロビジョニング
- 2025年9月:読み取り専用クエリの自動再試行(最大 2 回)、リモートバインディングがパブリックベータに
- 2025年7月:ストレージ上限を引き上げ(250GB → 1TB)、アルファ版のバックアップアクセスを削除、REST API が 50~500ms 高速化
- 2025年5月:HTTP API の権限に関するセキュリティ修正(書き込みには D1:Edit が必要)
- 2025年4月:読み取りレプリケーションがパブリックベータに(複数リージョンにまたがる読み取り専用レプリカ)
- 2025年2月:PRAGMA optimize のサポート、読み取り専用アクセス権限のバグ修正
- 2025年1月:無料プランの上限適用(2月10日開始)、Worker API のクエリが 40~60% 高速化
クイックスタート(5 分)
1. D1 データベースを作成する
# Create a new D1 database
npx wrangler d1 create my-database
# Output includes database_id - save this!
# ✅ Successfully created DB 'my-database'
#
# [[d1_databases]]
# binding = "DB"
# database_name = "my-database"
# database_id = "<UUID>"
2. バインディングを設定する
wrangler.jsonc に追加します。
{
"name": "my-worker",
"main": "src/index.ts",
"compatibility_date": "2025-10-11",
"d1_databases": [
{
"binding": "DB", // Available as env.DB in your Worker
"database_name": "my-database", // Name from wrangler d1 create
"database_id": "<UUID>", // ID from wrangler d1 create
"preview_database_id": "local-db" // For local development
}
]
}
重要:
bindingはコード内でデータベースにアクセスするための名前です(env.DB)database_idは本番データベースの UUID ですpreview_database_idはローカル開発用です(任意の文字列を指定できます)- 実際の
database_idの値を公開リポジトリにコミットしないでください。環境変数またはシークレットを使用してください
3. 最初のマイグレーションを作成する
# Create migration file
npx wrangler d1 migrations create my-database create_users_table
# This creates: migrations/0001_create_users_table.sql
マイグレーションファイルを編集します。
-- migrations/0001_create_users_table.sql
DROP TABLE IF EXISTS users;
CREATE TABLE IF NOT EXISTS users (
user_id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL UNIQUE,
username TEXT NOT NULL,
created_at INTEGER NOT NULL,
updated_at INTEGER
);
-- Create index for common queries
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Optimize database
PRAGMA optimize;
4. マイグレーションを適用する
# Apply locally first (for testing)
npx wrangler d1 migrations apply my-database --local
# Apply to production when ready
npx wrangler d1 migrations apply my-database --remote
5. Worker からクエリを実行する
// src/index.ts
import { Hono } from 'hono';
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
app.get('/api/users/:email', async (c) => {
const email = c.req.param('email');
try {
// ALWAYS use prepared statements with bind()
const result = await c.env.DB.prepare(
'SELECT * FROM users WHERE email = ?'
)
.bind(email)
.first();
if (!result) {
return c.json({ error: 'User not found' }, 404);
}
return c.json(result);
} catch (error: any) {
console.error('D1 Error:', error.message);
return c.json({ error: 'Database error' }, 500);
}
});
export default app;
D1 マイグレーションシステム
マイグレーションのワークフロー
# 1. Create migration
npx wrangler d1 migrations create <DATABASE_NAME> <MIGRATION_NAME>
# 2. List unapplied migrations
npx wrangler d1 migrations list <DATABASE_NAME> --local
npx wrangler d1 migrations list <DATABASE_NAME> --remote
# 3. Apply migrations
npx wrangler d1 migrations apply <DATABASE_NAME> --local # Test locally
npx wrangler d1 migrations apply <DATABASE_NAME> --remote # Deploy to production
マイグレーションファイルの命名
マイグレーションは自動的にバージョン管理されます。
migrations/
├── 0000_initial_schema.sql
├── 0001_add_users_table.sql
├── 0002_add_posts_table.sql
└── 0003_add_indexes.sql
ルール:
- ファイルは順番に実行されます
- 各マイグレーションは 1 回だけ実行されます(
d1_migrationsテーブルで追跡) - 失敗したマイグレーションはロールバックされます(トランザクション処理)
- 適用済みのマイグレーションは変更または削除できません
カスタムマイグレーション設定
{
"d1_databases": [
{
"binding": "DB",
"database_name": "my-database",
"database_id": "<UUID>",
"migrations_dir": "db/migrations", // Custom directory (default: migrations/)
"migrations_table": "schema_migrations" // Custom tracking table (default: d1_migrations)
}
]
}
マイグレーションのベストプラクティス
✅ 必ず行うこと:
-- Use IF NOT EXISTS to make migrations idempotent
CREATE TABLE IF NOT EXISTS users (...);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
-- Run PRAGMA optimize after schema changes
PRAGMA optimize;
-- Use transactions for data migrations
BEGIN TRANSACTION;
UPDATE users SET updated_at = unixepoch() WHERE updated_at IS NULL;
COMMIT;
❌ 絶対に行わないこと:
-- DON'T include BEGIN TRANSACTION at start (D1 handles this)
BEGIN TRANSACTION; -- ❌ Remove this
-- DON'T use MySQL/PostgreSQL syntax
ALTER TABLE users MODIFY COLUMN email VARCHAR(255); -- ❌ Not SQLite
-- DON'T create tables without IF NOT EXISTS
CREATE TABLE users (...); -- ❌ Fails if table exists
マイグレーションでの外部キーの処理
-- Temporarily disable foreign key checks during schema changes
PRAGMA defer_foreign_keys = true;
-- Make schema changes that would violate foreign keys
ALTER TABLE posts DROP COLUMN author_id;
ALTER TABLE posts ADD COLUMN user_id INTEGER REFERENCES users(user_id);
-- Foreign keys re-enabled automatically at end of migration
D1 Workers API
型定義:
interface Env { DB: D1Database; }
type Bindings = { DB: D1Database; };
const app = new Hono<{ Bindings: Bindings }>();
prepare() - 主なメソッド(ユーザー入力には必ず使用):
const user = await env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(email).first();
理由:SQL インジェクションを防止し、再利用可能で、パフォーマンスに優れ、型安全です
クエリ結果のメソッド:
.all()→{ results, meta }- すべての行を取得.first()→ 行オブジェクトまたは null - 最初の行を取得.first('column')→ 値 - 単一の列の値を取得(例:COUNT).run()→{ success, meta }- INSERT/UPDATE/DELETE を実行(結果なし)
batch() - パフォーマンス上、非常に重要:
const results = await env.DB.batch([
env.DB.prepare('SELECT * FROM users WHERE user_id = ?').bind(1),
env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(1)
]);
- 順番に実行し、ネットワークの往復は 1 回だけです
- 1 つ失敗すると、残りのステートメントは実行されません
- 使用例:一括挿入、関連データの取得
exec() - 本番環境では使用を避ける:
await env.DB.exec('SELECT * FROM users;'); // Only for migrations/maintenance
- ❌ ユーザー入力と一緒に使用しないでください(SQL インジェクションのリスク)
- ✅ 使用できるのは、マイグレーションファイルや単発タスクのみです
クエリのパターン
基本的な CRUD 操作
// CREATE
const { meta } = await env.DB.prepare(
'INSERT INTO users (email, username, created_at) VALUES (?, ?, ?)'
).bind(email, username, Date.now()).run();
const newUserId = meta.last_row_id;
// READ (single)
const user = await env.DB.prepare('SELECT * FROM users WHERE user_id = ?')
.bind(userId).first();
// READ (multiple)
const { results } = await env.DB.prepare('SELECT * FROM users LIMIT ?')
.bind(10).all();
// UPDATE
const { meta } = await env.DB.prepare('UPDATE users SET username = ? WHERE user_id = ?')
.bind(newUsername, userId).run();
const rowsAffected = meta.rows_written;
// DELETE
await env.DB.prepare('DELETE FROM users WHERE user_id = ?').bind(userId).run();
// COUNT
const count = await env.DB.prepare('SELECT COUNT(*) as total FROM users').first('total');
// EXISTS check
const exists = await env.DB.prepare('SELECT 1 FROM users WHERE email = ? LIMIT 1')
.bind(email).first();
ページネーションのパターン
const page = parseInt(c.req.query('page') || '1');
const limit = 20;
const offset = (page - 1) * limit;
const [countResult, usersResult] = await c.env.DB.batch([
c.env.DB.prepare('SELECT COUNT(*) as total FROM users'),
c.env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?')
.bind(limit, offset)
]);
return c.json({
users: usersResult.results,
pagination: { page, limit, total: countResult.results[0].total }
});
バッチのパターン(疑似トランザクション)
// D1 doesn't support multi-statement transactions, but batch() provides sequential execution
await env.DB.batch([
env.DB.prepare('UPDATE users SET credits = credits - ? WHERE user_id = ?').bind(amount, fromUserId),
env.DB.prepare('UPDATE users SET credits = credits + ? WHERE user_id = ?').bind(amount, toUserId),
env.DB.prepare('INSERT INTO transactions (from_user, to_user, amount) VALUES (?, ?, ?)').bind(fromUserId, toUserId, amount)
]);
// If any statement fails, batch stops (transaction-like behavior)
エラー処理
一般的なエラーの種類:
D1_ERROR- 一般的な D1 エラーD1_EXEC_ERROR- SQL 構文エラーD1_TYPE_ERROR- 型の不一致(null の代わりに undefined)D1_COLUMN_NOTFOUND- カラムが存在しません
よくあるエラーと修正方法:
| エラー | 原因 | 解決策 |
|---|---|---|
| ステートメントが長すぎます | 1000 行以上の大規模な INSERT | batch() を使って 100~250 行ずつに分割します |
| キューに登録されたリクエストが多すぎます | ループ内で個別にクエリを実行 | ループの代わりに batch() を使います |
| D1_TYPE_ERROR | bind で undefined を使用 | オプションの値には null を使います:.bind(email, bio || null) |
| トランザクションの競合 | マイグレーション内の BEGIN TRANSACTION | BEGIN/COMMIT を削除します(D1 が自動的に処理します) |
| 外部キー制約違反 | スキーマ変更によって制約が破損 | PRAGMA defer_foreign_keys = true を使います |
自動再試行(2025 年 9 月):
D1 は再試行可能なエラーが発生した場合、読み取り専用クエリ(SELECT、EXPLAIN、WITH)を最大 2 回、自動的に再試行します。再試行回数を確認するには、レスポンスの meta.total_attempts を確認してください。
パフォーマンスの最適化
インデックスのベストプラクティス:
- ✅ WHERE 句で使うカラムにインデックスを作成します:
CREATE INDEX idx_users_email ON users(email) - ✅ 外部キーにインデックスを作成します:
CREATE INDEX idx_posts_user_id ON posts(user_id) - ✅ ソートに使うカラムにインデックスを作成します:
CREATE INDEX idx_posts_created_at ON posts(created_at DESC) - ✅ 複数カラムのインデックスを作成します:
CREATE INDEX idx_posts_user_published ON posts(user_id, published) - ✅ 部分インデックスを作成します:
CREATE INDEX idx_users_active ON users(email) WHERE deleted = 0 - ✅ 次のコマンドでテストします:
EXPLAIN QUERY PLAN SELECT ...
PRAGMA optimize(2025 年 2 月):
CREATE INDEX idx_users_email ON users(email);
PRAGMA optimize; -- Run after schema changes
クエリの最適化:
- ✅ 特定のカラムを指定します(
SELECT *は使いません) - ✅ 大規模な結果セットには必ず LIMIT を指定します
- ✅ WHERE 条件にはインデックスを使います
- ❌ WHERE 句で関数を使うのは避けます(インデックスを使用できません):
WHERE LOWER(email)→ 代わりに小文字で保存します
ローカル開発
ローカルとリモートの比較(2025 年 11 月 - リモートバインディングの GA):
# Local database (automatic creation)
npx wrangler d1 migrations apply my-database --local
npx wrangler d1 execute my-database --local --command "SELECT * FROM users"
# Remote database
npx wrangler d1 execute my-database --remote --command "SELECT * FROM users"
# Remote bindings ([email protected]+) - connect local Worker to deployed D1
# Add to wrangler.jsonc: { "binding": "DB", "remote": true }
ローカルデータベースの場所:
.wrangler/state/v3/d1/miniflare-D1DatabaseObject/<database_id>.sqlite
ローカルデータベースへのデータ投入:
npx wrangler d1 execute my-database --local --file=seed.sql
ベストプラクティスのまとめ
✅ 必ず行うこと:
- ユーザー入力には
.bind()を使ってプリペアドステートメントを使用する - 複数のクエリには
.batch()を使う(レイテンシーを低減します) - 頻繁にクエリするカラムにインデックスを作成する
- スキーマ変更後に
PRAGMA optimizeを実行する - 冪等性を確保するため、マイグレーションで
IF NOT EXISTSを使う - 本番環境に適用する前にローカルでマイグレーションをテストする
- try/catch を使ってエラーを適切に処理する
- オプションの値には
undefinedではなくnullを使う - クエリにバインドする前に入力を検証する
- UPDATE/DELETE の後に
meta.rows_writtenを確認する
❌ 絶対に行わないこと:
- ユーザー入力に
.exec()を絶対に使わない(SQL インジェクションのリスクがあります) - 公開リポジトリに
database_idを絶対にハードコードしない - bind パラメーターに
undefinedを絶対に使わない(D1_TYPE_ERROR の原因になります) - ループ内で個別のクエリを絶対に実行しない(代わりに batch を使います)
- 大規模になる可能性がある結果セットで
LIMITを絶対に忘れない - 本番環境で
SELECT *を絶対に使わない(カラムを指定します) - マイグレーションファイルに
BEGIN TRANSACTIONを絶対に含めない - 適用済みのマイグレーションを絶対に変更しない(新しいものを作成します)
- データベース操作でのエラー処理を絶対に省略しない
- クエリが成功すると絶対に決めつけない(必ず結果を確認します)
防止される既知の問題
| 問題 | 説明 | 回避方法 |
|---|---|---|
| ステートメントが長すぎる | 大きな INSERT ステートメントが D1 の制限を超えます | 100~250 行ずつに分割します |
| トランザクションの競合 | マイグレーションファイル内の BEGIN TRANSACTION | BEGIN/COMMIT を削除します(D1 が処理します) |
| 外部キー制約違反 | スキーマの変更によって外部キー制約が破られます | PRAGMA defer_foreign_keys = true を使用します |
| レート制限/キューの過負荷 | 個別のクエリが多すぎます | ループではなく batch() を使用します |
| メモリ制限超過 | クエリがメモリに大量のデータを読み込みます | LIMIT を追加し、結果をページ分割し、クエリをシャーディングします |
| 型の不一致エラー | null の代わりに undefined を使用しています | オプションの値には必ず null を使用します |
Wrangler コマンドのリファレンス
# Database management
wrangler d1 create <DATABASE_NAME>
wrangler d1 list
wrangler d1 delete <DATABASE_NAME>
wrangler d1 info <DATABASE_NAME>
# Migrations
wrangler d1 migrations create <DATABASE_NAME> <MIGRATION_NAME>
wrangler d1 migrations list <DATABASE_NAME> --local|--remote
wrangler d1 migrations apply <DATABASE_NAME> --local|--remote
# Execute queries
wrangler d1 execute <DATABASE_NAME> --local|--remote --command "SELECT * FROM users"
wrangler d1 execute <DATABASE_NAME> --local|--remote --file=./query.sql
# Time Travel (view historical data)
wrangler d1 time-travel info <DATABASE_NAME> --timestamp "2025-10-20"
wrangler d1 time-travel restore <DATABASE_NAME> --timestamp "2025-10-20"
公式ドキュメント
- D1 の概要: https://developers.cloudflare.com/d1/
- はじめに: https://developers.cloudflare.com/d1/get-started/
- マイグレーション: https://developers.cloudflare.com/d1/reference/migrations/
- Workers API: https://developers.cloudflare.com/d1/worker-api/
- ベストプラクティス: https://developers.cloudflare.com/d1/best-practices/
- Wrangler コマンド: https://developers.cloudflare.com/workers/wrangler/commands/#d1
D1 での構築を始めましょう! 🚀