Cloudflare-D1
The easiest way to add SQL to your application
Schema-SQL-Migration-Worker API:
TypeScript Schema
│
│ drizzle-kit generate
▼
SQL Migration
│
│ Wrangler
▼
D1
│
│ drizzle-orm
▼
Worker API这里有一个很重要的分工:
- Drizzle ORM:TypeScript ↔ 数据库查询
- Drizzle Kit:Schema → Migration
- Wrangler:Cloudflare Worker / D1 的开发、部署和 D1 migration 执行
- D1:真正存数据
Drizzle 官方把这种方式称为 codebase-first:TypeScript schema 是数据库结构的 source of truth,Drizzle Kit 根据 schema 变化生成 SQL migration。
一、先看最终项目
我们做一个用户 API:
GET /users
POST /users项目:
my-api/
├── src/
│ ├── db/
│ │ └── schema.ts
│ └── index.ts
│
├── migrations/
│ ├── 0000_xxx/
│ │ ├── migration.sql
│ │ └── snapshot.json
│ └── 0001_xxx/
│ ├── migration.sql
│ └── snapshot.json
│
├── drizzle.config.ts
├── wrangler.jsonc
├── package.json
└── tsconfig.json注意:
这里的 migration 目录和我们之前纯 Wrangler 的例子稍微不同。
Drizzle Kit 默认会生成自己的 migration 结构;Cloudflare D1 现在也支持通过 migrations_pattern 配置识别这种嵌套 migration 布局。
二、安装
先创建 Worker:
npm create cloudflare@latest my-api
cd my-api
npm install安装 Drizzle:
npm install drizzle-orm
npm install -D drizzle-kitDrizzle 官方的 D1 文档也是使用 drizzle-orm + drizzle-kit 这套组合。
三、先创建线上 D1
这一步还是 Wrangler:
npx wrangler login然后:
npx wrangler d1 create my-api-db假设得到:
database_name = "my-api-db"
database_id = "abc123..."注意:
这个命令创建的是 Cloudflare 上的 D1,不是本地数据库。
四、配置 Wrangler
wrangler.jsonc:
{
"name": "my-api",
"main": "src/index.ts",
"compatibility_date": "2026-09-15",
"d1_databases": [
{
"binding": "DB",
"database_name": "my-api-db",
"database_id": "abc123...",
"migrations_dir": "migrations"
}
]
}这里:
binding = DB最终 Worker 里面:
env.DB就是 D1。
Drizzle 对 D1 的基本连接方式也是:
const db = drizzle(env.DB);五、写 TypeScript Schema
现在最关键的一步来了。
创建:
src/db/schema.ts写:
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
createdAt: text("created_at")
.notNull()
.$defaultFn(() => new Date().toISOString()),
});注意:
我们现在没有写 SQL。
没有:
CREATE TABLE users ...而是写 TypeScript。
这就是 Drizzle 的核心体验:
schema.ts
│
│ TypeScript
▼
Drizzle Kit
│
│ 自动 diff
▼
migration.sql六、告诉 Drizzle Schema 在哪里
创建:
drizzle.config.ts内容:
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./migrations",
dialect: "sqlite",
});这里最重要的是:
schema: "./src/db/schema.ts"以及:
out: "./migrations"意思就是:
我的数据库模型在这里;你生成的 migration 放这里。
Drizzle Kit 的 generate 会读取当前 schema,并和之前 migration 的 snapshot 比较,然后生成新的 SQL migration。
七、第一次生成 Migration
现在执行:
npx drizzle-kit generate --name initDrizzle 会分析:
src/db/schema.ts发现你定义了:
users
├── id
├── name
├── email
└── created_at于是自动生成 migration。
大概会得到:
migrations/
└── 0000_init/
├── migration.sql
└── snapshot.jsonmigration.sql 里面类似:
CREATE TABLE `users` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`name` text NOT NULL,
`email` text NOT NULL,
`created_at` text NOT NULL
);
CREATE UNIQUE INDEX `users_email_unique`
ON `users` (`email`);这个 SQL 是 Drizzle 生成的。
所以现在你已经可以看到:
你写
│
▼
src/db/schema.ts
│
│ generate
▼
migration.sql
│
│ apply
▼
D1八、这时候不要直接修改 migration
假设生成了:
0000_init然后 commit:
git add .
git commit -m "init database schema"之后如果要修改数据库:
修改 Schema,而不是修改旧 migration。
例如现在我们增加:
age: integer("age"),变成:
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
age: integer("age"),
createdAt: text("created_at")
.notNull()
.$defaultFn(() => new Date().toISOString()),
});然后:
npx drizzle-kit generate --name add-ageDrizzle 会比较:
旧 Schema
↓
新 Schema发现:
users
+ age于是生成:
migrations/
├── 0000_init/
│ ├── migration.sql
│ └── snapshot.json
│
└── 0001_add-age/
├── migration.sql
└── snapshot.json里面可能是:
ALTER TABLE `users`
ADD `age` integer;这就是我们想要的 migration。
九、现在进入本地 D1
这里有一个特别容易混淆的地方:
Drizzle generate不会修改数据库。
它只是:
Schema → SQL所以:
npx drizzle-kit generate只是生成文件。
真正把 migration 应用到 D1,是下一步。
十、本地应用 Migration
因为我们用的是 Cloudflare D1,所以这里可以让 Wrangler 管理 migration:
npx wrangler d1 migrations apply my-api-db --local这时候:
migrations/
│
▼
Wrangler
│
▼
Local D1Cloudflare 官方的 d1 migrations apply 支持 --local 和 --remote 两种模式;--local 用于本地开发,--remote 用于远程 D1。
十一、启动 Worker
现在:
npx wrangler devWorker 启动。
然后:
http://localhost:8787访问的是:
本地 Worker
│
▼
Local D1而不是生产数据库。
十二、Worker 中使用 Drizzle
现在修改:
src/index.tsimport { drizzle } from "drizzle-orm/d1";
import { users } from "./db/schema";
interface Env {
DB: D1Database;
}
export default {
async fetch(
request: Request,
env: Env,
) {
const db = drizzle(env.DB);
const url = new URL(request.url);
if (
url.pathname === "/users" &&
request.method === "GET"
) {
const result = await db
.select()
.from(users);
return Response.json(result);
}
if (
url.pathname === "/users" &&
request.method === "POST"
) {
const body = await request.json<{
name: string;
email: string;
age?: number;
}>();
await db
.insert(users)
.values({
name: body.name,
email: body.email,
age: body.age,
});
return Response.json(
{ ok: true },
{ status: 201 },
);
}
return new Response("Not Found", {
status: 404,
});
},
};这里非常漂亮:
const db = drizzle(env.DB);然后:
db.select()
db.insert()完全不需要自己写:
SELECT ...
INSERT ...十三、测试
启动:
npx wrangler devPOST:
curl -X POST http://localhost:8787/users \
-H "Content-Type: application/json" \
-d '{"name":"Alice","email":"alice@example.com","age":25}'然后:
curl http://localhost:8787/users得到:
[
{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
"age": 25,
"createdAt": "..."
}
]整个本地链路:
Browser / curl
│
▼
wrangler dev
│
▼
Worker
│
▼
Drizzle ORM
│
▼
Local D1十四、然后我们继续修改 Schema
假设产品经理突然说:
用户还要有手机号。
你不用自己写:
ALTER TABLE...而是修改:
export const users = sqliteTable("users", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
email: text("email").notNull().unique(),
age: integer("age"),
phone: text("phone"),
createdAt: text("created_at")
.notNull()
.$defaultFn(() => new Date().toISOString()),
});然后:
npx drizzle-kit generate --name add-phone得到:
0002_add-phone/
migration.sql
snapshot.json大概:
ALTER TABLE `users`
ADD `phone` text;然后本地:
npx wrangler d1 migrations apply my-api-db --local再测试。
十五、最后部署生产
确认:
Schema
↓
Migration
↓
Local D1
↓
Worker
↓
测试都没问题。
这时候先把 production D1 migration 执行掉:
npx wrangler d1 migrations apply my-api-db --remoteCloudflare 会把尚未执行的 migration 应用到远程 D1。官方文档说明 apply 会按顺序应用未执行 migration,并记录 migration 状态;如果 migration 出错,会回滚该次 migration。
然后部署 Worker:
npx wrangler deploy最终:
Git
│
▼
┌───────────────┐
│ schema.ts │
└───────┬───────┘
│
drizzle generate
│
▼
migrations
│
┌───────┴────────┐
│ │
--local --remote
│ │
▼ ▼
Local D1 Production D1
│
│
wrangler deploy
│
▼
Production Worker十六、所以到底谁负责什么?
这是最值得记住的一张表:
| 工具 | 负责什么 |
|---|---|
| Drizzle Schema | 描述数据库结构 |
| Drizzle Kit | Schema → SQL Migration |
| Wrangler | 管理/部署 Cloudflare |
| D1 | 存数据 |
| Drizzle ORM | Worker 中查询 D1 |
也就是:
TypeScript
│
▼
Drizzle Schema
│
│ generate
▼
SQL Migration
│
│ Wrangler
▼
D1
▲
│
│ Drizzle ORM
│
Worker十七、一个非常重要的区别:generate、push、migrate
Drizzle 有三个命令特别容易搞混。
generate
npx drizzle-kit generate意思:
根据 Schema 生成 migration 文件。
不会直接修改数据库。
push
npx drizzle-kit push意思更接近:
把当前 Schema 直接推到数据库。
适合快速实验、开发阶段快速迭代;Drizzle 官方也明确把 push 定位为快速测试 schema 的方式。
生产环境我一般不建议把它作为主要 migration 流程。
migrate
npx drizzle-kit migrate意思:
执行 Drizzle 生成的 migration。
不过对于我们这个 Cloudflare D1 + Wrangler 项目,我更倾向于让:
npx drizzle-kit generate负责生成 migration
然后:
npx wrangler d1 migrations apply负责把 migration 应用到 D1。
这样 Cloudflare 的 D1 migration 生命周期由 Wrangler 管理,会更清晰。Cloudflare 自己的 D1 migration 系统也提供本地/远程 apply。
十八、我推荐的实际工作流
以后你可以形成肌肉记忆:
第一次
npx wrangler d1 create my-api-db然后:
schema.ts
wrangler.jsonc
drizzle.config.ts每次修改数据库
① 修改 schema.ts
↓
② generate
npx drizzle-kit generate --name add-something
↓
③ 查看 migration.sql
↓
④ 本地执行
npx wrangler d1 migrations apply my-api-db --local
↓
⑤ wrangler dev 测试
↓
⑥ commit最后上线:
⑦
npx wrangler d1 migrations apply my-api-db --remote
↓
⑧
npx wrangler deploy十九、还有一个生产级细节
如果你有:
staging
production不要让它们共用一个 D1。
最好:
my-api-staging-db
my-api-production-db然后:
schema.ts
│
▼
Drizzle generate
│
▼
migrations
/ \
/ \
staging production
│ │
▼ ▼
staging D1 production D1Migration 文件是同一套:
migrations/
├── 0000_init/
├── 0001_add-age/
└── 0002_add-phone/只是分别应用到不同数据库。
这样你的数据库结构版本是一致的,但数据完全隔离。
最后,你现在应该建立这个认知
以前你理解的是:
我手写 SQL
↓
Wrangler
↓
D1用了 Drizzle 后变成:
TypeScript
│
▼
schema.ts
│
│ Drizzle Kit
▼
migration.sql
│
│ Wrangler
▼
D1而运行时又是另一条线路:
HTTP Request
│
▼
Cloudflare Worker
│
│ drizzle-orm
▼
env.DB
│
▼
D1“Schema 管结构、Drizzle Kit 管 migration、Wrangler 管 Cloudflare、Drizzle ORM 管查询”——把这四句话记住,整个架构就不会乱。
我建议直接做一个**staging + production + GitHub Actions 的完整项目**。那时可以把 migration 放进 CI/CD,做到 git push → 自动部署 staging → 测试 → production,这才是真正用于生产时的样子。
Related Posts
什么是 Harness
模型只会生成文本。Harness 是把其中一部分文本变成工具调用、真正执行,再把结果送回模型的那一层运行时。
拆解 OpenAI Pando:用因果一致性和可复用 packfile 把 Git 扩到 agent 规模
OpenAI 的 Git 后端 Pando 把写入降为按 ref 串行化、只对 ref 指针做 CAS,读取则复用推送时的 pack、通过 Git 协议 v2 的 packfile-uri 从区域缓存分发。后台用类似 LSM 的分层压实,让拉取规划做到 O(log Δ)。
RSC
React Server Components (RSC) in Next.js are components that render exclusively on the server, making them the default component type in the App Router.