name: cloudflare-d1 description: Cloudflare D1 SQLite数据库,包含Workers、Drizzle ORM和迁移
Cloudflare D1 技能
加载方式:base.md + typescript.md
Cloudflare D1是为Cloudflare Workers设计的无服务器SQLite数据库,具有全球分发和零冷启动的特点。
资料来源: D1文档 | Drizzle + D1 | Wrangler CLI
核心原则
在边缘使用SQLite,迁移在版本控制中,Drizzle用于类型安全。
D1将SQLite的简单性带入无服务器环境。设计时考虑水平扩展(多个小型数据库)而不是垂直扩展(一个大型数据库)。使用Drizzle ORM进行类型安全的查询和迁移。
D1技术栈
| 组件 | 目的 |
|---|---|
| D1 | 无服务器SQLite数据库 |
| Workers | 应用程序的边缘运行时 |
| Wrangler | 开发和部署的CLI |
| Drizzle ORM | 带迁移的类型安全ORM |
| Drizzle Kit | 迁移工具 |
| Hono | 轻量级Web框架(可选) |
项目设置
创建Worker项目
# 创建新项目
npm create cloudflare@latest my-app -- --template "worker-typescript"
cd my-app
# 安装依赖
npm install drizzle-orm
npm install -D drizzle-kit
创建D1数据库
# 创建数据库(同时创建本地和远程)
npx wrangler d1 create my-database
# 输出:
# [[d1_databases]]
# binding = "DB"
# database_name = "my-database"
# database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
配置wrangler.toml
name = "my-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"
[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
migrations_dir = "drizzle"
migrations_table = "drizzle_migrations"
生成TypeScript类型
# 从wrangler.toml生成环境类型
npx wrangler types
# 创建worker-configuration.d.ts:
# interface Env {
# DB: D1Database;
# }
Drizzle ORM设置
模式定义
// src/db/schema.ts
import { sqliteTable, text, integer, real, blob } from 'drizzle-orm/sqlite-core';
import { sql } from 'drizzle-orm';
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name').notNull(),
role: text('role', { enum: ['user', 'admin'] }).default('user'),
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`)
});
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
published: integer('published', { mode: 'boolean' }).default(false),
viewCount: integer('view_count').default(0),
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`)
});
export const tags = sqliteTable('tags', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull().unique()
});
export const postTags = sqliteTable('post_tags', {
postId: integer('post_id').references(() => posts.id),
tagId: integer('tag_id').references(() => tags.id)
});
// 类型导出
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;
Drizzle配置
// drizzle.config.ts
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
schema: './src/db/schema.ts',
out: './drizzle',
dialect: 'sqlite',
driver: 'd1-http',
dbCredentials: {
accountId: process.env.CLOUDFLARE_ACCOUNT_ID!,
databaseId: process.env.CLOUDFLARE_DATABASE_ID!,
token: process.env.CLOUDFLARE_D1_TOKEN!
}
});
数据库客户端
// src/db/index.ts
import { drizzle } from 'drizzle-orm/d1';
import * as schema from './schema';
export function createDb(d1: D1Database) {
return drizzle(d1, { schema });
}
export type Database = ReturnType<typeof createDb>;
export * from './schema';
迁移工作流
生成迁移
# 从模式变化生成迁移
npx drizzle-kit generate
# 输出:drizzle/0000_initial.sql
本地应用迁移
# 应用到本地D1
npx wrangler d1 migrations apply my-database --local
# 或者通过Drizzle
npx drizzle-kit migrate
应用迁移到生产环境
# 应用到远程D1
npx wrangler d1 migrations apply my-database --remote
# 预览(dry run)
npx wrangler d1 migrations apply my-database --remote --dry-run
迁移文件示例
-- drizzle/0000_initial.sql
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`email` text NOT NULL,
`name` text NOT NULL,
`role` text DEFAULT 'user',
`created_at` text DEFAULT CURRENT_TIMESTAMP,
`updated_at` text DEFAULT CURRENT_TIMESTAMP
);
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
CREATE TABLE `posts` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`title` text NOT NULL,
`content` text,
`author_id` integer REFERENCES `users`(`id`),
`published` integer DEFAULT false,
`view_count` integer DEFAULT 0,
`created_at` text DEFAULT CURRENT_TIMESTAMP
);
Worker实现
基本Worker与Hono
// src/index.ts
import { Hono } from 'hono';
import { createDb, users, posts } from './db';
import { eq, desc } from 'drizzle-orm';
type Bindings = {
DB: D1Database;
};
const app = new Hono<{ Bindings: Bindings }>();
// 中间件注入db
app.use('*', async (c, next) => {
c.set('db', createDb(c.env.DB));
await next();
});
// 列出用户
app.get('/users', async (c) => {
const db = c.get('db');
const allUsers = await db.select().from(users);
return c.json(allUsers);
});
// 通过ID获取用户
app.get('/users/:id', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
const user = await db.select().from(users).where(eq(users.id, id)).get();
if (!user) {
return c.json({ error: '用户未找到' }, 404);
}
return c.json(user);
});
// 创建用户
app.post('/users', async (c) => {
const db = c.get('db');
const body = await c.req.json<{ email: string; name: string }>();
const result = await db.insert(users).values({
email: body.email,
name: body.name
}).returning();
return c.json(result[0], 201);
});
// 更新用户
app.put('/users/:id', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
const body = await c.req.json<Partial<{ email: string; name: string }>>();
const result = await db.update(users)
.set({ ...body, updatedAt: new Date().toISOString() })
.where(eq(users.id, id))
.returning();
if (result.length === 0) {
return c.json({ error: '用户未找到' }, 404);
}
return c.json(result[0]);
});
// 删除用户
app.delete('/users/:id', async (c) => {
const db = c.get('db');
const id = parseInt(c.req.param('id'));
const result = await db.delete(users).where(eq(users.id, id)).returning();
if (result.length === 0) {
return c.json({ error: '用户未找到' }, 404);
}
return c.json({ deleted: true });
});
export default app;
原始D1 API(无ORM)
// src/index.ts
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 ORDER BY created_at DESC'
).all();
return Response.json(results);
}
if (url.pathname === '/users' && request.method === 'POST') {
const body = await request.json() as { email: string; name: string };
const result = await env.DB.prepare(
'INSERT INTO users (email, name) VALUES (?, ?) RETURNING *'
).bind(body.email, body.name).first();
return Response.json(result, { status: 201 });
}
return new Response('Not Found', { status: 404 });
}
};
查询模式
选择查询
import { eq, and, or, like, gt, desc, asc, count, sql } from 'drizzle-orm';
// 基本选择
const allPosts = await db.select().from(posts);
// 选择特定列
const titles = await db.select({ id: posts.id, title: posts.title }).from(posts);
// 条件语句
const published = await db.select().from(posts).where(eq(posts.published, true));
// 多个条件
const recentPublished = await db.select().from(posts).where(
and(
eq(posts.published, true),
gt(posts.createdAt, '2024-01-01')
)
);
// OR条件
const featured = await db.select().from(posts).where(
or(
eq(posts.viewCount, 1000),
like(posts.title, '%featured%')
)
);
// 排序和限制
const topPosts = await db.select()
.from(posts)
.orderBy(desc(posts.viewCount))
.limit(10);
// 分页
const page2 = await db.select()
.from(posts)
.orderBy(desc(posts.createdAt))
.limit(10)
.offset(10);
// 计数
const postCount = await db.select({ count: count() }).from(posts);
连接
// 内连接
const postsWithAuthors = await db.select({
post: posts,
author: users
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id));
// 左连接
const allPostsWithAuthors = await db.select()
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id));
// 通过连接表的多对多
const postsWithTags = await db.select({
post: posts,
tag: tags
})
.from(posts)
.leftJoin(postTags, eq(posts.id, postTags.postId))
.leftJoin(tags, eq(postTags.tagId, tags.id));
插入、更新、删除
// 单个插入
const newUser = await db.insert(users).values({
email: 'user@example.com',
name: 'John Doe'
}).returning();
// 批量插入
await db.insert(users).values([
{ email: 'a@test.com', name: 'Alice' },
{ email: 'b@test.com', name: 'Bob' }
]);
// Upsert(冲突时插入或更新)
await db.insert(users)
.values({ email: 'user@test.com', name: 'New Name' })
.onConflictDoUpdate({
target: users.email,
set: { name: 'New Name' }
});
// 更新
await db.update(posts)
.set({ published: true })
.where(eq(posts.id, 1));
// 更新并增加
await db.update(posts)
.set({ viewCount: sql`${posts.viewCount} + 1` })
.where(eq(posts.id, 1));
// 删除
await db.delete(posts).where(eq(posts.id, 1));
事务
// D1通过批量支持事务
const results = await db.batch([
db.insert(users).values({ email: 'a@test.com', name: 'A' }),
db.insert(users).values({ email: 'b@test.com', name: 'B' }),
db.update(posts).set({ published: true }).where(eq(posts.id, 1))
]);
// 原始D1批量
const batchResults = await env.DB.batch([
env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind('a@test.com', 'A'),
env.DB.prepare('INSERT INTO users (email, name) VALUES (?, ?)').bind('b@test.com', 'B')
]);
本地开发
启动开发服务器
# 本地开发使用D1
npx wrangler dev
# 使用特定端口
npx wrangler dev --port 8787
数据库管理
# 本地执行SQL
npx wrangler d1 execute my-database --local --command "SELECT * FROM users"
# 执行SQL文件
npx wrangler d1 execute my-database --local --file ./seed.sql
# 打开SQLite shell
npx wrangler d1 execute my-database --local --command ".tables"
Drizzle Studio
# 运行Drizzle Studio进行可视化数据库管理
npx drizzle-kit studio
种子数据
-- seed.sql
INSERT INTO users (email, name, role) VALUES
('admin@example.com', 'Admin User', 'admin'),
('user@example.com', 'Test User', 'user');
INSERT INTO posts (title, content, author_id, published) VALUES
('First Post', 'Hello World!', 1, true),
('Draft Post', 'Work in progress...', 1, false);
# 本地数据库播种
npx wrangler d1 execute my-database --local --file ./seed.sql
多环境设置
wrangler.toml
name = "my-app"
main = "src/index.ts"
compatibility_date = "2024-01-01"
# 开发环境
[env.dev]
[[env.dev.d1_databases]]
binding = "DB"
database_name = "my-database-dev"
database_id = "dev-database-id"
# 暂存环境
[env.staging]
[[env.staging.d1_databases]]
binding = "DB"
database_name = "my-database-staging"
database_id = "staging-database-id"
# 生产环境
[env.production]
[[env.production.d1_databases]]
binding = "DB"
database_name = "my-database-prod"
database_id = "prod-database-id"
部署到环境
# 部署到暂存环境
npx wrangler deploy --env staging
# 部署到生产环境
npx wrangler deploy --env production
# 应用暂存环境的迁移
npx wrangler d1 migrations apply my-database-staging --remote --env staging
测试
集成测试
// tests/api.test.ts
import { unstable_dev } from 'wrangler';
import type { UnstableDevWorker } from 'wrangler';
import { describe, beforeAll, afterAll, it, expect } from 'vitest';
describe('API', () => {
let worker: UnstableDevWorker;
beforeAll(async () => {
worker = await unstable_dev('src/index.ts', {
experimental: { disableExperimentalWarning: true }
});
});
afterAll(async () => {
await worker.stop();
});
it('should list users', async () => {
const res = await worker.fetch('/users');
expect(res.status).toBe(200);
const data = await res.json();
expect(Array.isArray(data)).toBe(true);
});
it('should create user', async () => {
const res = await worker.fetch('/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@test.com', name: 'Test' })
});
expect(res.status).toBe(201);
});
});
CLI快速参考
# 数据库
wrangler d1 create <name> # 创建数据库
wrangler d1 list # 列出数据库
wrangler d1 info <name> # 数据库信息
wrangler d1 delete <name> # 删除数据库
# 迁移
wrangler d1 migrations list <name> # 列出迁移
wrangler d1 migrations apply <name> --local # 本地应用
wrangler d1 migrations apply <name> --remote # 生产应用
# SQL执行
wrangler d1 execute <name> --command "SQL" # 运行SQL
wrangler d1 execute <name> --file ./file.sql # 运行SQL文件
wrangler d1 execute <name> --local # 本地运行
wrangler d1 execute <name> --remote # 生产运行
# 开发
wrangler dev # 启动本地服务器
wrangler types # 生成TypeScript类型
wrangler deploy # 生产部署
# Drizzle
drizzle-kit generate # 生成迁移
drizzle-kit migrate # 应用迁移
drizzle-kit studio # 打开Drizzle Studio
drizzle-kit push # 推送模式(仅开发)
D1限制和考虑因素
| 限制 | 值 |
|---|---|
| 数据库大小 | 最大10 GB |
| 行大小 | 最大1 MB |
| SQL语句 | 最大100 KB |
| 批量大小 | 1000条语句 |
| 每日读取(免费) | 500万 |
| 每日写入(免费) | 10万 |
反模式
- 单一大型数据库 - 设计多个较小的数据库(每个租户)
- 无迁移 - 总是版本控制模式更改
- 到处使用原始SQL - 使用Drizzle进行类型安全
- 无连接到远程 - 总是在部署前测试真实的D1
- D1中的大BLOB - 使用R2进行文件存储
- 复杂连接 - D1是SQLite;保持查询简单
- 无批量 - 使用批量进行多个操作
- 忽略限制 - 监控免费层的使用情况