AI 电子伴侣
创建时间: 2026-04-18最后更新: 2026-04-18

1. 原生 SQL 的问题

上一篇我们用 D1 做了用户 CRUD,SQL 都是手写字符串。

回顾一下那段代码:

index.ts
1
app.get('/users/:id', async (c) => {
2
const id = c.req.param('id')
3
const result = await c.env.DB.prepare(
4
'SELECT * FROM users WHERE id = ?'
5
).bind(id).first()
6
// result 的类型是 any
7
// 字段名拼错了?编译器不会告诉你
8
return c.json({ user: result })
9
})

问题有三个:

  • 没有类型检查resultany,取 result.nmae 这种拼写错误编译器不报错
  • SQL 拼字符串:字段多了容易漏、容易错,IDE 帮不了你
  • 表结构散落各处:建表 SQL 在一个地方,查询在另一个地方,改了表结构你得全局搜代码

ORM 就是来解决这些问题的。它用 TypeScript 对象描述表结构,查询结果自动带类型。

2. 为什么选 Drizzle

市面上 Node.js ORM 主要有 Prisma 和 Drizzle 两个。

Prisma 的问题:它有自己的 schema 语言(.prisma 文件),查询语法和 SQL 差别很大,学习成本高。而且它的运行时比较重,在 Workers 这种边缘环境跑起来不太舒服。

Drizzle 的优势:

  • TypeScript 优先:schema 就是 TypeScript 代码,不需要额外的语言
  • SQL-like 语法select().from().where() 和 SQL 几乎一一对应,会写 SQL 就会用 Drizzle
  • 原生支持 D1:Cloudflare 官方推荐,适配没有额外开销
  • 零依赖、极轻量:打包后很小,适合 Workers 环境

3. 安装

terminal
1
npm install drizzle-orm
2
npm install -D drizzle-kit
  • drizzle-orm:运行时库,提供查询构建器
  • drizzle-kit:开发工具,负责生成迁移 SQL 和管理数据库 schema

4. 定义 Schema

Schema 是一切的起点。在项目根目录创建 src/db/schema.ts

src/db/schema.ts
01
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
02
import { sql } from 'drizzle-orm'
03
04
export const users = sqliteTable('users', {
05
id: integer('id').primaryKey({ autoIncrement: true }),
06
name: text('name').notNull(),
07
email: text('email').notNull().unique(),
08
role: text('role').default('user'),
09
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
10
})

几个要点:

  • D1 底层是 SQLite,所以用 drizzle-orm/sqlite-core 的类型
  • sqliteTable('users', {...}) 第一个参数是真实表名
  • 链式调用 .notNull().unique().default() 定义约束
  • sql 模板标签用于嵌入原生 SQL 表达式(比如 CURRENT_TIMESTAMP
  • users 变量既是查询时的表引用,也自动推导出了 TypeScript 类型

从 schema 中可以推导出类型,方便在其他地方使用:

src/db/schema.ts
1
import { InferSelectModel, InferInsertModel } from 'drizzle-orm'
2
3
// 查询结果的类型
4
type User = InferSelectModel<typeof users>
5
// { id: number; name: string; email: string; role: string | null; createdAt: string | null }
6
7
// 插入数据的类型(id、role、createdAt 是可选的)
8
type NewUser = InferInsertModel<typeof users>
9
// { id?: number; name: string; email: string; role?: string; createdAt?: string }

5. 初始化 Drizzle 实例

在 Hono 路由中,把 D1 binding 传给 Drizzle:

src/index.ts
01
import { Hono } from 'hono'
02
import { drizzle } from 'drizzle-orm/d1'
03
import { users } from './db/schema'
04
05
type Bindings = {
06
DB: D1Database
07
}
08
09
const app = new Hono<{ Bindings: Bindings }>()
10
11
app.get('/users', async (c) => {
12
const db = drizzle(c.env.DB)
13
const result = await db.select().from(users).all()
14
return c.json(result)
15
})
16
17
export default app

注意:每次请求都要调用 drizzle(c.env.DB) 创建新实例。Workers 是无状态的,没有持久化的数据库连接,这和传统 Node.js 服务不一样。不过别担心,drizzle() 只是一个轻量包装,没有性能问题。

6. CRUD 查询构建器

这是 Drizzle 最核心的部分。我们把每种操作和原生 SQL 对比着看。

查询所有

原生
1
const result = await c.env.DB
2
.prepare('SELECT * FROM users')
3
.all()
4
// result.results 是 any[]
Drizzle
1
const db = drizzle(c.env.DB)
2
const result = await db.select().from(users).all()
3
// result 是 User[],每个字段都有类型

条件查询

原生
1
const user = await c.env.DB
2
.prepare('SELECT * FROM users WHERE id = ?')
3
.bind(id)
4
.first()
5
// user 是 any
Drizzle
1
import { eq } from 'drizzle-orm'
2
3
const user = await db.select().from(users)
4
.where(eq(users.id, id))
5
.get()
6
// user 是 User | undefined

eq 是等于比较,Drizzle 还提供了一整套条件函数:

conditions.ts
01
import { eq, ne, gt, gte, lt, lte, like, and, or, isNull } from 'drizzle-orm'
02
03
// 等于 / 不等于
04
eq(users.role, 'admin') // role = 'admin'
05
ne(users.role, 'admin') // role != 'admin'
06
07
// 大小比较
08
gt(users.id, 10) // id > 10
09
gte(users.id, 10) // id >= 10
10
11
// 模糊匹配
12
like(users.name, '%张%') // name LIKE '%张%'
13
14
// 组合条件
15
and(eq(users.role, 'admin'), gt(users.id, 5)) // role = 'admin' AND id > 5
16
or(eq(users.role, 'admin'), eq(users.role, 'editor')) // role = 'admin' OR role = 'editor'
17
18
// NULL 检查
19
isNull(users.createdAt) // created_at IS NULL

插入

原生
1
const result = await c.env.DB
2
.prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *')
3
.bind(name, email)
4
.first()
Drizzle
1
const result = await db.insert(users)
2
.values({ name, email })
3
.returning()
4
.get()
5
// result 是 User,字段带类型
6
// 如果 name 拼成了 nmae,TypeScript 直接报错

更新

原生
1
await c.env.DB
2
.prepare('UPDATE users SET name = ? WHERE id = ?')
3
.bind(name, id)
4
.run()
Drizzle
1
await db.update(users)
2
.set({ name })
3
.where(eq(users.id, id))
4
.run()

删除

原生
1
await c.env.DB
2
.prepare('DELETE FROM users WHERE id = ?')
3
.bind(id)
4
.run()
Drizzle
1
await db.delete(users)
2
.where(eq(users.id, id))
3
.run()

对比下来,Drizzle 的语法和 SQL 几乎一一对应,但多了类型安全。写错字段名、传错类型,编译阶段就能发现。

7. 关联查询

假设我们有一个 posts 表,每个 post 属于一个 user:

src/db/schema.ts
01
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
02
import { sql, relations } from 'drizzle-orm'
03
04
export const users = sqliteTable('users', {
05
id: integer('id').primaryKey({ autoIncrement: true }),
06
name: text('name').notNull(),
07
email: text('email').notNull().unique(),
08
role: text('role').default('user'),
09
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
10
})
11
12
export const posts = sqliteTable('posts', {
13
id: integer('id').primaryKey({ autoIncrement: true }),
14
title: text('title').notNull(),
15
content: text('content').notNull(),
16
authorId: integer('author_id').notNull().references(() => users.id),
17
})
18
19
// 定义关系
20
export const usersRelations = relations(users, ({ many }) => ({
21
posts: many(posts),
22
}))
23
24
export const postsRelations = relations(posts, ({ one }) => ({
25
author: one(users, {
26
fields: [posts.authorId],
27
references: [users.id],
28
}),
29
}))

使用 db.query 做关联查询:

index.ts
01
import * as schema from './db/schema'
02
03
const db = drizzle(c.env.DB, { schema })
04
05
// 查询用户及其所有文章
06
const usersWithPosts = await db.query.users.findMany({
07
with: {
08
posts: true,
09
},
10
})
11
// 类型自动推导:{ id: number; name: string; ...; posts: Post[] }[]
12
13
// 查询文章及其作者
14
const postsWithAuthor = await db.query.posts.findMany({
15
with: {
16
author: true,
17
},
18
})

注意两点:

  • 使用 db.query 需要在 drizzle() 初始化时传入 { schema }
  • relations() 只是告诉 Drizzle 表之间的关系,不会生成外键约束的 SQL,外键靠 .references() 定义

8. 迁移工作流

Schema 定义好了,怎么同步到数据库?这就是 drizzle-kit 的工作。

配置 drizzle-kit

在项目根目录创建 drizzle.config.ts

drizzle.config.ts
1
import { defineConfig } from 'drizzle-kit'
2
3
export default defineConfig({
4
schema: './src/db/schema.ts',
5
out: './drizzle/migrations',
6
dialect: 'sqlite',
7
})

生成迁移文件

terminal
1
npx drizzle-kit generate

这会读取你的 schema,和上一次的状态对比,生成一个 SQL 迁移文件:

code.ts
1
drizzle/migrations/
2
0000_create_users.sql
3
0001_add_posts.sql

打开看看,就是标准的 SQL:

0000_create_users.sql
1
CREATE TABLE `users` (
2
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
3
`name` text NOT NULL,
4
`email` text NOT NULL,
5
`role` text DEFAULT 'user',
6
`created_at` text DEFAULT CURRENT_TIMESTAMP
7
);
8
CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);

应用迁移

用 wrangler 把迁移 SQL 跑到 D1 上:

terminal
1
# 本地开发环境
2
npx wrangler d1 migrations apply my-database --local
3
4
# 远程生产环境
5
npx wrangler d1 migrations apply my-database --remote

整个流程就是:改 schema -> generate -> apply,三步走。

9. 完整示例:用 Drizzle 重写用户 CRUD

把前面的知识点串起来,完整的代码如下:

src/db/schema.ts
01
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
02
import { sql } from 'drizzle-orm'
03
04
export const users = sqliteTable('users', {
05
id: integer('id').primaryKey({ autoIncrement: true }),
06
name: text('name').notNull(),
07
email: text('email').notNull().unique(),
08
role: text('role').default('user'),
09
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
10
})
src/index.ts
01
import { Hono } from 'hono'
02
import { drizzle } from 'drizzle-orm/d1'
03
import { eq } from 'drizzle-orm'
04
import { users } from './db/schema'
05
06
type Bindings = {
07
DB: D1Database
08
}
09
10
const app = new Hono<{ Bindings: Bindings }>()
11
12
// 获取所有用户
13
app.get('/users', async (c) => {
14
const db = drizzle(c.env.DB)
15
const allUsers = await db.select().from(users).all()
16
return c.json(allUsers)
17
})
18
19
// 获取单个用户
20
app.get('/users/:id', async (c) => {
21
const db = drizzle(c.env.DB)
22
const id = Number(c.req.param('id'))
23
const user = await db.select().from(users)
24
.where(eq(users.id, id))
25
.get()
26
27
if (!user) {
28
return c.json({ error: 'User not found' }, 404)
29
}
30
return c.json(user)
31
})
32
33
// 创建用户
34
app.post('/users', async (c) => {
35
const db = drizzle(c.env.DB)
36
const { name, email } = await c.req.json()
37
38
const newUser = await db.insert(users)
39
.values({ name, email })
40
.returning()
41
.get()
42
43
return c.json(newUser, 201)
44
})
45
46
// 更新用户
47
app.put('/users/:id', async (c) => {
48
const db = drizzle(c.env.DB)
49
const id = Number(c.req.param('id'))
50
const { name, email } = await c.req.json()
51
52
const updated = await db.update(users)
53
.set({ name, email })
54
.where(eq(users.id, id))
55
.returning()
56
.get()
57
58
if (!updated) {
59
return c.json({ error: 'User not found' }, 404)
60
}
61
return c.json(updated)
62
})
63
64
// 删除用户
65
app.delete('/users/:id', async (c) => {
66
const db = drizzle(c.env.DB)
67
const id = Number(c.req.param('id'))
68
69
const deleted = await db.delete(users)
70
.where(eq(users.id, id))
71
.returning()
72
.get()
73
74
if (!deleted) {
75
return c.json({ error: 'User not found' }, 404)
76
}
77
return c.json({ message: 'Deleted', user: deleted })
78
})
79
80
export default app

和上一篇的原生 SQL 版本对比,代码量差不多,但每一行都有类型保护。重构时改了 schema 的字段名,TypeScript 编译器会帮你把所有用到的地方都标红。

10. Workers 环境注意事项

在 Cloudflare Workers 中使用 Drizzle 有几点和传统 Node.js 不同:

  • 每次请求新建实例drizzle(c.env.DB) 要在请求处理函数里调用,不能放在顶层。Workers 每次请求的 c.env.DB 可能不同
  • 没有连接池:D1 是 HTTP 协议访问的,不存在 TCP 连接池的概念,所以不需要配置连接数
  • 事务支持有限:D1 支持事务,但 Drizzle 的 db.transaction() 在 D1 驱动下有些限制,复杂事务建议用 db.batch() 代替
  • 批量操作用 batch:D1 支持批量执行多条 SQL,Drizzle 也暴露了这个能力
batch.ts
01
const db = drizzle(c.env.DB)
02
03
// 批量执行:一次网络请求发送多条 SQL
04
const results = await db.batch([
05
db.insert(users).values({ name: 'Alice', email: 'alice@example.com' }),
06
db.insert(users).values({ name: 'Bob', email: 'bob@example.com' }),
07
db.select().from(users).all(),
08
])
09
// results[0] 是第一条 insert 的结果
10
// results[1] 是第二条 insert 的结果
11
// results[2] 是 select 的结果

总结

Drizzle ORM 解决了原生 SQL 的三个痛点:类型安全、字段校验、schema 集中管理。它的 SQL-like 语法没有太多学习成本,又能享受 TypeScript 的编译时检查。

核心流程:定义 schema -> 生成迁移 -> 写查询。

下一篇我们来看 Cloudflare R2 对象存储,处理文件上传的场景。