Cloudflare D1データベース活用ガイド
Cloudflare D1は、Cloudflare Workers上で動作するサーバーレスSQLiteデータベースです。エッジコンピューティング環境でのデータ永続化を実現し、低レイテンシーでスケーラブルなアプリケーション開発が可能になります。本記事では、D1の基礎から実践的な使い方まで詳しく解説します。
Cloudflare D1とは
D1は、Cloudflareのエッジネットワーク上で動作するSQLiteベースのリレーショナルデータベースサービスです。
主な特徴
1. エッジでのデータ永続化
- Cloudflare Workers内でSQLを実行
- グローバルに分散されたデータベース
2. SQLite互換
- 標準的なSQL構文をサポート
- 既存のSQLite知識を活用可能
3. サーバーレス
- インフラ管理不要
- 自動スケーリング
4. 低コスト
- 無料枠あり
- 使った分だけの従量課金
セットアップ
Wranglerのインストール
npm install -g wrangler
# ログイン
wrangler login
プロジェクト作成
# 新規プロジェクト作成
npm create cloudflare@latest my-d1-app
cd my-d1-app
D1データベースの作成
# データベース作成
wrangler d1 create my-database
# 出力例:
# Created DB with id: xxxx-xxxx-xxxx-xxxx
# [[d1_databases]]
# binding = "DB"
# database_name = "my-database"
# database_id = "xxxx-xxxx-xxxx-xxxx"
wrangler.tomlの設定
name = "my-d1-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxx-xxxx-xxxx-xxxx"
スキーマ定義
マイグレーションファイルの作成
schema.sql:
-- ユーザーテーブル
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT NOT NULL UNIQUE,
email TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 投稿テーブル
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
);
-- インデックス
CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
マイグレーション実行
# ローカル環境で実行
wrangler d1 execute my-database --local --file=./schema.sql
# 本番環境で実行
wrangler d1 execute my-database --file=./schema.sql
基本的なクエリ
Worker内でのD1使用
src/index.ts:
export interface Env {
DB: D1Database;
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
// ユーザー一覧取得
if (url.pathname === "/users" && request.method === "GET") {
const { results } = await env.DB.prepare(
"SELECT * FROM users"
).all();
return Response.json(results);
}
// ユーザー作成
if (url.pathname === "/users" && request.method === "POST") {
const { username, email } = await request.json();
const result = await env.DB.prepare(
"INSERT INTO users (username, email) VALUES (?, ?)"
)
.bind(username, email)
.run();
return Response.json({
id: result.meta.last_row_id,
username,
email
}, { status: 201 });
}
return new Response("Not found", { status: 404 });
}
};
パラメータバインディング
// プレースホルダーを使った安全なクエリ
const getUserByEmail = async (email: string, env: Env) => {
const result = await env.DB.prepare(
"SELECT * FROM users WHERE email = ?"
)
.bind(email)
.first();
return result;
};
// 複数パラメータ
const createPost = async (userId: number, title: string, content: string, env: Env) => {
const result = await env.DB.prepare(
"INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)"
)
.bind(userId, title, content)
.run();
return result.meta.last_row_id;
};
CRUD操作
Create (作成)
async function createUser(
username: string,
email: string,
env: Env
) {
const result = await env.DB.prepare(
`INSERT INTO users (username, email)
VALUES (?, ?)
RETURNING id, username, email, created_at`
)
.bind(username, email)
.first();
return result;
}
Read (読取)
// 全件取得
async function getAllUsers(env: Env) {
const { results } = await env.DB.prepare(
"SELECT * FROM users ORDER BY created_at DESC"
).all();
return results;
}
// 単一レコード取得
async function getUserById(id: number, env: Env) {
const user = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
)
.bind(id)
.first();
return user;
}
// ページネーション
async function getUsersPaginated(
page: number,
perPage: number,
env: Env
) {
const offset = (page - 1) * perPage;
const { results } = await env.DB.prepare(
"SELECT * FROM users LIMIT ? OFFSET ?"
)
.bind(perPage, offset)
.all();
return results;
}
Update (更新)
async function updateUser(
id: number,
username: string,
email: string,
env: Env
) {
const result = await env.DB.prepare(
`UPDATE users
SET username = ?, email = ?
WHERE id = ?
RETURNING *`
)
.bind(username, email, id)
.first();
return result;
}
Delete (削除)
async function deleteUser(id: number, env: Env) {
const result = await env.DB.prepare(
"DELETE FROM users WHERE id = ?"
)
.bind(id)
.run();
return result.meta.changes > 0;
}
トランザクション
async function transferPost(
postId: number,
fromUserId: number,
toUserId: number,
env: Env
) {
const results = await env.DB.batch([
env.DB.prepare(
"UPDATE posts SET user_id = ? WHERE id = ? AND user_id = ?"
).bind(toUserId, postId, fromUserId),
env.DB.prepare(
"INSERT INTO post_transfers (post_id, from_user_id, to_user_id) VALUES (?, ?, ?)"
).bind(postId, fromUserId, toUserId),
]);
return results.every(r => r.success);
}
JOIN操作
// ユーザーと投稿を結合
async function getUsersWithPosts(env: Env) {
const { results } = await env.DB.prepare(
`SELECT
users.id,
users.username,
users.email,
COUNT(posts.id) as post_count
FROM users
LEFT JOIN posts ON users.id = posts.user_id
GROUP BY users.id
ORDER BY post_count DESC`
).all();
return results;
}
// 投稿詳細(ユーザー情報含む)
async function getPostWithUser(postId: number, env: Env) {
const post = await env.DB.prepare(
`SELECT
posts.*,
users.username,
users.email
FROM posts
INNER JOIN users ON posts.user_id = users.id
WHERE posts.id = ?`
)
.bind(postId)
.first();
return post;
}
フルCRUD API実装例
import { Router } from 'itty-router';
export interface Env {
DB: D1Database;
}
const router = Router();
// ユーザー一覧
router.get('/api/users', async (request, env: Env) => {
const url = new URL(request.url);
const page = parseInt(url.searchParams.get('page') || '1');
const perPage = parseInt(url.searchParams.get('per_page') || '10');
const offset = (page - 1) * perPage;
const [users, count] = await Promise.all([
env.DB.prepare(
"SELECT * FROM users ORDER BY created_at DESC LIMIT ? OFFSET ?"
)
.bind(perPage, offset)
.all(),
env.DB.prepare("SELECT COUNT(*) as total FROM users")
.first<{ total: number }>()
]);
return Response.json({
users: users.results,
pagination: {
page,
per_page: perPage,
total: count?.total || 0,
total_pages: Math.ceil((count?.total || 0) / perPage)
}
});
});
// ユーザー詳細
router.get('/api/users/:id', async (request, env: Env) => {
const { id } = request.params;
const user = await env.DB.prepare(
"SELECT * FROM users WHERE id = ?"
)
.bind(id)
.first();
if (!user) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return Response.json(user);
});
// ユーザー作成
router.post('/api/users', async (request, env: Env) => {
try {
const { username, email } = await request.json();
if (!username || !email) {
return Response.json(
{ error: 'Username and email are required' },
{ status: 400 }
);
}
const user = await env.DB.prepare(
`INSERT INTO users (username, email)
VALUES (?, ?)
RETURNING *`
)
.bind(username, email)
.first();
return Response.json(user, { status: 201 });
} catch (error) {
return Response.json(
{ error: 'Failed to create user' },
{ status: 500 }
);
}
});
// ユーザー更新
router.put('/api/users/:id', async (request, env: Env) => {
const { id } = request.params;
const { username, email } = await request.json();
const user = await env.DB.prepare(
`UPDATE users
SET username = ?, email = ?
WHERE id = ?
RETURNING *`
)
.bind(username, email, id)
.first();
if (!user) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return Response.json(user);
});
// ユーザー削除
router.delete('/api/users/:id', async (request, env: Env) => {
const { id } = request.params;
const result = await env.DB.prepare(
"DELETE FROM users WHERE id = ?"
)
.bind(id)
.run();
if (result.meta.changes === 0) {
return Response.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return new Response(null, { status: 204 });
});
export default {
fetch: router.handle
};
ローカル開発
# ローカル開発サーバー起動
wrangler dev --local
# ローカルD1コンソール
wrangler d1 execute my-database --local --command "SELECT * FROM users"
デプロイ
# 本番環境にデプロイ
wrangler deploy
# マイグレーション実行
wrangler d1 execute my-database --file=./schema.sql
パフォーマンス最適化
インデックスの活用
-- 頻繁に検索されるカラムにインデックス
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);
-- 複合インデックス
CREATE INDEX idx_posts_user_created ON posts(user_id, created_at);
バッチ処理
// 効率的な一括挿入
async function bulkInsertUsers(
users: Array<{ username: string; email: string }>,
env: Env
) {
const statements = users.map(user =>
env.DB.prepare(
"INSERT INTO users (username, email) VALUES (?, ?)"
).bind(user.username, user.email)
);
const results = await env.DB.batch(statements);
return results;
}
まとめ
Cloudflare D1は、エッジコンピューティング環境でデータ永続化を実現する革新的なサービスです。
主な利点
- 低レイテンシー: エッジでのデータアクセス
- スケーラビリティ: 自動スケーリング
- 簡単な統合: Workers との seamless な統合
- コスト効率: 無料枠と従量課金
グローバルに分散されたアプリケーションで、高速なデータアクセスが必要な場合、D1は優れた選択肢となるでしょう。