mcpField
C
cloudflare-d1

cloudflare-d1

github.com/jackspaceデータ・データベース1641 回インストール12 週間前に更新
リポジトリ

cloudflare-d1 について

cloudflare-d1は、Cloudflare WorkersでD1データベースを使う開発者向けのガイドです。設定、マイグレーション、クエリに加え、プリペアドステートメントやバッチ処理も扱います。

主な機能

  1. 01D1データベースのバインディング設定
  2. 02SQLマイグレーションの作成と適用
  3. 03プリペアドステートメントでのクエリ
  4. 04複数ステートメントのバッチ実行
  5. 05クエリとインデックスの最適化

活用シーン

  1. 01Worker向けのD1設定
  2. 02ローカルとリモートのマイグレーション管理
  3. 03WorkerでのCRUD操作の実装

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/jackspace/claudeskillz --skill cloudflare-d1

インストール後、Claude Code で/cloudflare-d1を入力して読み込まれたか確認します。

同梱ファイルREADME.mdSKILL.json

詳細

Cloudflare D1 Database

ステータス:本番環境で使用可能 ✅ 最終更新日:2025-10-21 依存関係:cloudflare-worker-base(Worker のセットアップ用) 最新バージョン:[email protected]、@cloudflare/[email protected]


クイックスタート(5 分)

1. D1 Database を作成する
# 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

ルール:

  • ファイルは順番に実行されます。
  • 各マイグレーションは一度だけ実行されます(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

型定義
// Add to env.d.ts or worker-configuration.d.ts
interface Env {
  DB: D1Database;
  // ... other bindings
}

// For Hono
type Bindings = {
  DB: D1Database;
};

const app = new Hono<{ Bindings: Bindings }>();
prepare() - プリペアドステートメント(主要な方法)

ユーザー入力を含むクエリには、必ずプリペアドステートメントを使用してください。

// Basic prepared statement
const stmt = env.DB.prepare('SELECT * FROM users WHERE user_id = ?');
const bound = stmt.bind(userId);
const result = await bound.first();

// Chained (most common pattern)
const user = await env.DB.prepare('SELECT * FROM users WHERE email = ?')
  .bind(email)
  .first();

prepare() を使用する理由:

  • ✅ SQL インジェクションを防止します。
  • ✅ 異なるパラメーターで再利用できます。
  • ✅ パフォーマンスが向上します(クエリプランのキャッシュ)。
  • ✅ TypeScript で型安全です。
クエリ結果のメソッド
.all() - すべての行を取得する
const { results, meta } = await env.DB.prepare(
  'SELECT * FROM users WHERE created_at > ?'
)
.bind(timestamp)
.all();

console.log(results);  // Array of rows
console.log(meta);     // { duration, rows_read, rows_written }
.first() - 最初の行を取得する
// Returns first row or null
const user = await env.DB.prepare('SELECT * FROM users WHERE email = ?')
  .bind('[email protected]')
  .first();

if (!user) {
  return c.json({ error: 'Not found' }, 404);
}
.first(column) - 単一の列の値を取得する
// Returns the value of a specific column from first row
const count = await env.DB.prepare('SELECT COUNT(*) as total FROM users')
  .first('total');

console.log(count);  // 42 (just the number, not an object)
.run() - 結果を取得せずに実行する
// For INSERT, UPDATE, DELETE
const { success, meta } = await env.DB.prepare(
  'INSERT INTO users (email, username, created_at) VALUES (?, ?, ?)'
)
.bind(email, username, Date.now())
.run();

console.log(meta);  // { duration, rows_read, rows_written, last_row_id }
batch() - 複数のクエリを実行する

パフォーマンスにとって非常に重要:batch() を使用してレイテンシを削減してください。

// Prepare multiple statements
const stmt1 = env.DB.prepare('SELECT * FROM users WHERE user_id = ?').bind(1);
const stmt2 = env.DB.prepare('SELECT * FROM users WHERE user_id = ?').bind(2);
const stmt3 = env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(1);

// Execute all in one round trip
const results = await env.DB.batch([stmt1, stmt2, stmt3]);

console.log(results[0].results);  // Users query 1
console.log(results[1].results);  // Users query 2
console.log(results[2].results);  // Posts query

バッチの動作:

  • 順番に実行されます。
  • 各ステートメントは個別にコミットされます(自動コミットモード)。
  • いずれかが失敗すると、残りのステートメントは実行されません。
  • 個別のクエリよりも大幅に高速です(ネットワークの往復が 1 回)。

バッチの使用例:

// ✅ Insert multiple rows efficiently
const inserts = users.map(user =>
  env.DB.prepare('INSERT INTO users (email, username) VALUES (?, ?)')
    .bind(user.email, user.username)
);
await env.DB.batch(inserts);

// ✅ Fetch related data in parallel
const [user, posts, comments] = await env.DB.batch([
  env.DB.prepare('SELECT * FROM users WHERE user_id = ?').bind(userId),
  env.DB.prepare('SELECT * FROM posts WHERE user_id = ?').bind(userId),
  env.DB.prepare('SELECT * FROM comments WHERE user_id = ?').bind(userId)
]);
exec() - 生の SQL を実行する(本番環境では使用しないでください)
// Only for migrations, maintenance, and one-off tasks
const result = await env.DB.exec(`
  SELECT * FROM users;
  SELECT * FROM posts;
`);

console.log(result);  // { count: 2, duration: 5 }

次の場合、exec() を絶対に使用しないでください:

  • ❌ ユーザー入力を含むクエリ(SQL インジェクションのリスクがあります)。
  • ❌ 本番環境のクエリ(パフォーマンスが低下します)。
  • ❌ 結果が必要なクエリ(exec はデータを返しません)。

次の場合にのみ exec() を使用してください:

  • ✅ ローカルでのマイグレーション SQL ファイルの実行
  • ✅ 一度限りの保守タスク
  • ✅ データベース初期化スクリプト

クエリパターン

基本的な CRUD 操作
作成(INSERT)
// Single insert
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;

// Bulk insert with batch()
const users = [
  { email: '[email protected]', username: 'user1' },
  { email: '[email protected]', username: 'user2' }
];

const inserts = users.map(u =>
  env.DB.prepare('INSERT INTO users (email, username, created_at) VALUES (?, ?, ?)')
    .bind(u.email, u.username, Date.now())
);

await env.DB.batch(inserts);
読み取り(SELECT)
// Single row
const user = await env.DB.prepare('SELECT * FROM users WHERE user_id = ?')
  .bind(userId)
  .first();

// Multiple rows
const { results } = await env.DB.prepare(
  'SELECT * FROM users WHERE created_at > ? ORDER BY created_at DESC LIMIT ?'
)
.bind(timestamp, 10)
.all();

// 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();

if (exists) {
  // Email already registered
}
更新(UPDATE)
const { meta } = await env.DB.prepare(
  'UPDATE users SET username = ?, updated_at = ? WHERE user_id = ?'
)
.bind(newUsername, Date.now(), userId)
.run();

const rowsAffected = meta.rows_written;

if (rowsAffected === 0) {
  // User not found
}
削除(DELETE)
const { meta } = await env.DB.prepare('DELETE FROM users WHERE user_id = ?')
  .bind(userId)
  .run();

const rowsDeleted = meta.rows_written;
高度なクエリ
ページネーション
app.get('/api/users', async (c) => {
  const page = parseInt(c.req.query('page') || '1');
  const limit = parseInt(c.req.query('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)
  ]);

  const total = countResult.results[0].total as number;
  const users = usersResult.results;

  return c.json({
    users,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit)
    }
  });
});
JOIN
const { results } = await env.DB.prepare(`
  SELECT
    posts.*,
    users.username as author_name,
    users.email as author_email
  FROM posts
  INNER JOIN users ON posts.user_id = users.user_id
  WHERE posts.published = ?
  ORDER BY posts.created_at DESC
  LIMIT ?
`)
.bind(1, 10)
.all();
トランザクション(バッチパターン)

D1 は複数のステートメントによるトランザクションをサポートしていませんが、batch() を使うと順番に実行できます。

// Transfer credits between users (pseudo-transaction)
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)
]);

注:いずれかのステートメントが失敗すると、バッチは停止します。これにより、トランザクションに似た動作が一部実現されます。


エラー処理

エラーの種類
try {
  const result = await env.DB.prepare('SELECT * FROM users WHERE user_id = ?')
    .bind(userId)
    .first();
} catch (error: any) {
  // D1 errors have a message property
  const errorMessage = error.message;

  if (errorMessage.includes('D1_ERROR')) {
    // D1-specific error
  } else if (errorMessage.includes('D1_EXEC_ERROR')) {
    // SQL syntax error
  } else if (errorMessage.includes('D1_TYPE_ERROR')) {
    // Type mismatch (e.g., undefined instead of null)
  } else if (errorMessage.includes('D1_COLUMN_NOTFOUND')) {
    // Column doesn't exist
  }

  console.error('Database error:', errorMessage);
  return c.json({ error: 'Database operation failed' }, 500);
}
よくあるエラーと修正方法
「Statement too long」
// ❌ DON'T: Single massive INSERT
await env.DB.exec(`
  INSERT INTO users (email) VALUES
    ('[email protected]'),
    ('[email protected]'),
    ... // 1000 more rows
`);

// ✅ DO: Break into batches
const batchSize = 100;
for (let i = 0; i < users.length; i += batchSize) {
  const batch = users.slice(i, i + batchSize);
  const inserts = batch.map(u =>
    env.DB.prepare('INSERT INTO users (email) VALUES (?)').bind(u.email)
  );
  await env.DB.batch(inserts);
}
「Too many requests queued」
// ❌ DON'T: Fire off many individual queries
for (const user of users) {
  await env.DB.prepare('INSERT INTO users (email) VALUES (?)').bind(user.email).run();
}

// ✅ DO: Use batch()
const inserts = users.map(u =>
  env.DB.prepare('INSERT INTO users (email) VALUES (?)').bind(u.email)
);
await env.DB.batch(inserts);
「D1_TYPE_ERROR」(undefined と null)
// ❌ DON'T: Use undefined
await env.DB.prepare('INSERT INTO users (email, bio) VALUES (?, ?)')
  .bind(email, undefined);  // ❌ D1 doesn't support undefined

// ✅ DO: Use null for optional values
await env.DB.prepare('INSERT INTO users (email, bio) VALUES (?, ?)')
  .bind(email, bio || null);
再試行ロジック
async function queryWithRetry<T>(
  queryFn: () => Promise<T>,
  maxRetries = 3
): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await queryFn();
    } catch (error: any) {
      const message = error.message;

      // Retry on transient errors
      const isRetryable =
        message.includes('Network connection lost') ||
        message.includes('storage caused object to be reset') ||
        message.includes('reset because its code was updated');

      if (!isRetryable || attempt === maxRetries - 1) {
        throw error;
      }

      // Exponential backoff
      const delay = Math.min(1000 * Math.pow(2, attempt), 5000);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error('Retry logic failed');
}

// Usage
const user = await queryWithRetry(() =>
  env.DB.prepare('SELECT * FROM users WHERE user_id = ?')
    .bind(userId)
    .first()
);

パフォーマンスの最適化

インデックス

インデックスを作成すると、フィルター対象の列を使うクエリのパフォーマンスが大幅に向上します。

インデックスを作成するタイミング
// ✅ Index columns used in WHERE clauses
CREATE INDEX idx_users_email ON users(email);

// ✅ Index foreign keys
CREATE INDEX idx_posts_user_id ON posts(user_id);

// ✅ Index columns used for sorting
CREATE INDEX idx_posts_created_at ON posts(created_at DESC);

// ✅ Multi-column indexes for complex queries
CREATE INDEX idx_posts_user_published ON posts(user_id, published);
インデックスの使用状況をテストする
-- Check if index is being used
EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = '[email protected]';

-- Should see: SEARCH users USING INDEX idx_users_email
部分インデックス
-- Index only non-deleted records
CREATE INDEX idx_users_active ON users(email) WHERE deleted = 0;

-- Index only published posts
CREATE INDEX idx_posts_published ON posts(created_at DESC) WHERE published = 1;
PRAGMA optimize

インデックスの作成後、またはスキーマの変更後に実行します。

-- In your migration file
CREATE INDEX idx_users_email ON users(email);
PRAGMA optimize;

または Worker から実行します。

await env.DB.exec('PRAGMA optimize');
クエリ最適化のヒント
// ✅ Use specific columns instead of SELECT *
const users = await env.DB.prepare(
  'SELECT user_id, email, username FROM users'
).all();

// ✅ Use LIMIT to prevent scanning entire table
const latest = await env.DB.prepare(
  'SELECT * FROM posts ORDER BY created_at DESC LIMIT 10'
).all();

// ✅ Use indexes for WHERE conditions
// Create index first: CREATE INDEX idx_users_email ON users(email)
const user = await env.DB.prepare('SELECT * FROM users WHERE email = ?')
  .bind(email)
  .first();

// ❌ Avoid functions in WHERE (can't use indexes)
// Bad: WHERE LOWER(email) = '[email protected]'
// Good: WHERE email = '[email protected]' (store email lowercase)

ローカル開発

ローカルデータベースとリモートデータベース
# Create local database (automatic on first --local command)
npx wrangler d1 migrations apply my-database --local

# Query local database
npx wrangler d1 execute my-database --local --command "SELECT * FROM users"

# Query remote database
npx wrangler d1 execute my-database --remote --command "SELECT * FROM users"
ローカルデータベースの場所

ローカルの D1 データベースは次の場所に保存されます。

.wrangler/state/v3/d1/miniflare-D1DatabaseObject/<database_id>.sqlite
ローカルデータベースへの初期データ投入
# Create seed file
cat > seed.sql << 'EOF'
INSERT INTO users (email, username, created_at) VALUES
  ('[email protected]', 'alice', 1698000000),
  ('[email protected]', 'bob', 1698000060);
EOF

# Apply seed
npx wrangler d1 execute my-database --local --file=seed.sql

Drizzle ORM(任意)

D1 は生の SQL で十分に機能しますが、ORM を好む開発者もいます。Drizzle ORM は D1 に対応しています。

npm install drizzle-orm
npm install -D drizzle-kit

注記:Drizzle を導入すると複雑さが増し、新たに習得するレイヤーも加わります。ほとんどの D1 のユースケースでは、wrangler を使った生の SQL のほうがシンプルで直接的です。Drizzle を検討するのは、次のような場合に限ってください。

  • SQL よりも TypeScript によるスキーマ定義を好む
  • クエリの自動補完を利用したい
  • 複雑なスキーマを持つ非常に大規模なアプリケーションを構築している

Drizzle の公式 D1 ドキュメント:https://orm.drizzle.team/docs/get-started-sqlite#cloudflare-d1


ベストプラクティスのまとめ

✅ 必ず実行すること:
  1. ユーザー入力には .bind() を使ってプリペアドステートメントを使用する
  2. 複数のクエリには .batch() を使用する(レイテンシが低減します)
  3. 頻繁にクエリされる列にインデックスを作成する
  4. スキーマ変更後に PRAGMA optimize を実行する
  5. 冪等性を確保するため、マイグレーションで IF NOT EXISTS を使用する
  6. 本番環境に適用する前にマイグレーションをローカルでテストする
  7. try/catch を使ってエラーを適切に処理する
  8. オプション値には undefined ではなく null を使用する
  9. クエリにバインドする前に入力を検証する
  10. UPDATE/DELETE の後に meta.rows_written を確認する
❌ 絶対にしないこと:
  1. ユーザー入力に .exec() を絶対に使用しない(SQL インジェクションのリスクがあります)
  2. 公開リポジトリで database_id を絶対にハードコードしない
  3. バインドパラメーターに undefined を絶対に使用しない(D1_TYPE_ERROR が発生します)
  4. ループ内で個別のクエリを絶対に実行しない(代わりに batch を使用してください)
  5. 大きな結果セットになる可能性がある場合、LIMIT を絶対に忘れない
  6. 本番環境で SELECT * を絶対に使用しない(列を指定してください)
  7. マイグレーションファイルに BEGIN TRANSACTION を絶対に含めない
  8. 適用済みのマイグレーションを絶対に変更しない(新しいものを作成してください)
  9. データベース操作でのエラー処理を絶対に省略しない
  10. クエリが成功すると絶対に決めつけない(必ず結果を確認してください)

防止できる既知の問題

問題説明回避方法
ステートメントが長すぎる大きな INSERT ステートメントが D1 の制限を超える100~250 行のバッチに分割する
トランザクションの競合マイグレーションファイル内の BEGIN TRANSACTIONBEGIN/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 での構築を始めましょう! 🚀

コメント