关于 cloudflare-d1
cloudflare-d1 是面向 Cloudflare Workers 开发者的 D1 数据库操作指南,涵盖配置、迁移与查询,并强调使用预处理语句和批处理。
主要功能
- 01配置 D1 数据库绑定
- 02创建和应用 SQL 迁移
- 03使用预处理语句查询
- 04批量执行多条语句
- 05优化查询与索引
适用场景
- 01为 Worker 配置 D1 数据库
- 02管理本地与远程数据库迁移
- 03在 Worker 中实现 CRUD 操作
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill cloudflare-d1安装后在 Claude Code 中输入/cloudflare-d1确认已加载。
附带文件README.mdSKILL.json
详细介绍
Cloudflare D1 数据库
状态:已可用于生产环境 ✅ 最后更新:2025-10-21 依赖项:cloudflare-worker-base(用于 Worker 设置) 最新版本:[email protected]、@cloudflare/[email protected]
快速入门(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是生产数据库 UUIDpreview_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
批处理行为:
- 按顺序依次执行
- 每条语句单独提交(自动提交模式)
- 如果某条语句失败,后续语句不会执行
- 比单独执行查询快得多(只需一次网络往返)
批处理的使用场景:
// ✅ 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)
}
});
});
联接
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:
- 更喜欢使用 TypeScript 定义架构,而不是 SQL
- 希望查询支持自动补全
- 正在构建架构复杂的大型应用
Drizzle D1 官方文档:https://orm.drizzle.team/docs/get-started-sqlite#cloudflare-d1
最佳实践总结
✅ 始终要做:
- 对用户输入使用预处理语句和
.bind() - 多个查询使用
.batch()(可降低延迟) - 为经常查询的列创建索引
- 更改架构后运行
PRAGMA optimize - 在迁移中使用
IF NOT EXISTS,确保幂等性 - 应用到生产环境前在本地测试迁移
- 使用 try/catch 妥善处理错误
- 对可选值使用
null,而不是undefined - 绑定到查询之前验证输入
- 执行 UPDATE/DELETE 后检查
meta.rows_written
❌ 绝不要做:
- 绝不要在用户输入上使用
.exec()(有 SQL 注入风险) - 绝不要在公开代码仓库中硬编码
database_id - 绝不要在绑定参数中使用
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、分页获取结果、拆分查询 |
| 类型不匹配错误 | 使用 undefined 而不是 null | 可选值始终使用 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 开始构建了! 🚀