1. 原生 SQL 的问题
上一篇我们用 D1 做了用户 CRUD,SQL 都是手写字符串。
回顾一下那段代码:
1app.get('/users/:id', async (c) => {2const id = c.req.param('id')3const result = await c.env.DB.prepare(4'SELECT * FROM users WHERE id = ?'5).bind(id).first()6// result 的类型是 any7// 字段名拼错了?编译器不会告诉你8return c.json({ user: result })9})
问题有三个:
- 没有类型检查:
result是any,取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. 安装
1npm install drizzle-orm2npm install -D drizzle-kit
- drizzle-orm:运行时库,提供查询构建器
- drizzle-kit:开发工具,负责生成迁移 SQL 和管理数据库 schema
4. 定义 Schema
Schema 是一切的起点。在项目根目录创建 src/db/schema.ts:
01import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'02import { sql } from 'drizzle-orm'0304export const users = sqliteTable('users', {05id: integer('id').primaryKey({ autoIncrement: true }),06name: text('name').notNull(),07email: text('email').notNull().unique(),08role: text('role').default('user'),09createdAt: 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 中可以推导出类型,方便在其他地方使用:
1import { InferSelectModel, InferInsertModel } from 'drizzle-orm'23// 查询结果的类型4type User = InferSelectModel<typeof users>5// { id: number; name: string; email: string; role: string | null; createdAt: string | null }67// 插入数据的类型(id、role、createdAt 是可选的)8type NewUser = InferInsertModel<typeof users>9// { id?: number; name: string; email: string; role?: string; createdAt?: string }
5. 初始化 Drizzle 实例
在 Hono 路由中,把 D1 binding 传给 Drizzle:
01import { Hono } from 'hono'02import { drizzle } from 'drizzle-orm/d1'03import { users } from './db/schema'0405type Bindings = {06DB: D1Database07}0809const app = new Hono<{ Bindings: Bindings }>()1011app.get('/users', async (c) => {12const db = drizzle(c.env.DB)13const result = await db.select().from(users).all()14return c.json(result)15})1617export default app
注意:每次请求都要调用 drizzle(c.env.DB) 创建新实例。Workers 是无状态的,没有持久化的数据库连接,这和传统 Node.js 服务不一样。不过别担心,drizzle() 只是一个轻量包装,没有性能问题。
6. CRUD 查询构建器
这是 Drizzle 最核心的部分。我们把每种操作和原生 SQL 对比着看。
查询所有
1const result = await c.env.DB2.prepare('SELECT * FROM users')3.all()4// result.results 是 any[]
1const db = drizzle(c.env.DB)2const result = await db.select().from(users).all()3// result 是 User[],每个字段都有类型
条件查询
1const user = await c.env.DB2.prepare('SELECT * FROM users WHERE id = ?')3.bind(id)4.first()5// user 是 any
1import { eq } from 'drizzle-orm'23const user = await db.select().from(users)4.where(eq(users.id, id))5.get()6// user 是 User | undefined
eq 是等于比较,Drizzle 还提供了一整套条件函数:
01import { eq, ne, gt, gte, lt, lte, like, and, or, isNull } from 'drizzle-orm'0203// 等于 / 不等于04eq(users.role, 'admin') // role = 'admin'05ne(users.role, 'admin') // role != 'admin'0607// 大小比较08gt(users.id, 10) // id > 1009gte(users.id, 10) // id >= 101011// 模糊匹配12like(users.name, '%张%') // name LIKE '%张%'1314// 组合条件15and(eq(users.role, 'admin'), gt(users.id, 5)) // role = 'admin' AND id > 516or(eq(users.role, 'admin'), eq(users.role, 'editor')) // role = 'admin' OR role = 'editor'1718// NULL 检查19isNull(users.createdAt) // created_at IS NULL
插入
1const result = await c.env.DB2.prepare('INSERT INTO users (name, email) VALUES (?, ?) RETURNING *')3.bind(name, email)4.first()
1const result = await db.insert(users)2.values({ name, email })3.returning()4.get()5// result 是 User,字段带类型6// 如果 name 拼成了 nmae,TypeScript 直接报错
更新
1await c.env.DB2.prepare('UPDATE users SET name = ? WHERE id = ?')3.bind(name, id)4.run()
1await db.update(users)2.set({ name })3.where(eq(users.id, id))4.run()
删除
1await c.env.DB2.prepare('DELETE FROM users WHERE id = ?')3.bind(id)4.run()
1await db.delete(users)2.where(eq(users.id, id))3.run()
对比下来,Drizzle 的语法和 SQL 几乎一一对应,但多了类型安全。写错字段名、传错类型,编译阶段就能发现。
7. 关联查询
假设我们有一个 posts 表,每个 post 属于一个 user:
01import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'02import { sql, relations } from 'drizzle-orm'0304export const users = sqliteTable('users', {05id: integer('id').primaryKey({ autoIncrement: true }),06name: text('name').notNull(),07email: text('email').notNull().unique(),08role: text('role').default('user'),09createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),10})1112export const posts = sqliteTable('posts', {13id: integer('id').primaryKey({ autoIncrement: true }),14title: text('title').notNull(),15content: text('content').notNull(),16authorId: integer('author_id').notNull().references(() => users.id),17})1819// 定义关系20export const usersRelations = relations(users, ({ many }) => ({21posts: many(posts),22}))2324export const postsRelations = relations(posts, ({ one }) => ({25author: one(users, {26fields: [posts.authorId],27references: [users.id],28}),29}))
使用 db.query 做关联查询:
01import * as schema from './db/schema'0203const db = drizzle(c.env.DB, { schema })0405// 查询用户及其所有文章06const usersWithPosts = await db.query.users.findMany({07with: {08posts: true,09},10})11// 类型自动推导:{ id: number; name: string; ...; posts: Post[] }[]1213// 查询文章及其作者14const postsWithAuthor = await db.query.posts.findMany({15with: {16author: true,17},18})
注意两点:
- 使用
db.query需要在drizzle()初始化时传入{ schema } relations()只是告诉 Drizzle 表之间的关系,不会生成外键约束的 SQL,外键靠.references()定义
8. 迁移工作流
Schema 定义好了,怎么同步到数据库?这就是 drizzle-kit 的工作。
配置 drizzle-kit
在项目根目录创建 drizzle.config.ts:
1import { defineConfig } from 'drizzle-kit'23export default defineConfig({4schema: './src/db/schema.ts',5out: './drizzle/migrations',6dialect: 'sqlite',7})
生成迁移文件
1npx drizzle-kit generate
这会读取你的 schema,和上一次的状态对比,生成一个 SQL 迁移文件:
1drizzle/migrations/20000_create_users.sql30001_add_posts.sql
打开看看,就是标准的 SQL:
1CREATE 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_TIMESTAMP7);8CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`);
应用迁移
用 wrangler 把迁移 SQL 跑到 D1 上:
1# 本地开发环境2npx wrangler d1 migrations apply my-database --local34# 远程生产环境5npx wrangler d1 migrations apply my-database --remote
整个流程就是:改 schema -> generate -> apply,三步走。
9. 完整示例:用 Drizzle 重写用户 CRUD
把前面的知识点串起来,完整的代码如下:
01import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'02import { sql } from 'drizzle-orm'0304export const users = sqliteTable('users', {05id: integer('id').primaryKey({ autoIncrement: true }),06name: text('name').notNull(),07email: text('email').notNull().unique(),08role: text('role').default('user'),09createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),10})
01import { Hono } from 'hono'02import { drizzle } from 'drizzle-orm/d1'03import { eq } from 'drizzle-orm'04import { users } from './db/schema'0506type Bindings = {07DB: D1Database08}0910const app = new Hono<{ Bindings: Bindings }>()1112// 获取所有用户13app.get('/users', async (c) => {14const db = drizzle(c.env.DB)15const allUsers = await db.select().from(users).all()16return c.json(allUsers)17})1819// 获取单个用户20app.get('/users/:id', async (c) => {21const db = drizzle(c.env.DB)22const id = Number(c.req.param('id'))23const user = await db.select().from(users)24.where(eq(users.id, id))25.get()2627if (!user) {28return c.json({ error: 'User not found' }, 404)29}30return c.json(user)31})3233// 创建用户34app.post('/users', async (c) => {35const db = drizzle(c.env.DB)36const { name, email } = await c.req.json()3738const newUser = await db.insert(users)39.values({ name, email })40.returning()41.get()4243return c.json(newUser, 201)44})4546// 更新用户47app.put('/users/:id', async (c) => {48const db = drizzle(c.env.DB)49const id = Number(c.req.param('id'))50const { name, email } = await c.req.json()5152const updated = await db.update(users)53.set({ name, email })54.where(eq(users.id, id))55.returning()56.get()5758if (!updated) {59return c.json({ error: 'User not found' }, 404)60}61return c.json(updated)62})6364// 删除用户65app.delete('/users/:id', async (c) => {66const db = drizzle(c.env.DB)67const id = Number(c.req.param('id'))6869const deleted = await db.delete(users)70.where(eq(users.id, id))71.returning()72.get()7374if (!deleted) {75return c.json({ error: 'User not found' }, 404)76}77return c.json({ message: 'Deleted', user: deleted })78})7980export 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 也暴露了这个能力
01const db = drizzle(c.env.DB)0203// 批量执行:一次网络请求发送多条 SQL04const results = await db.batch([05db.insert(users).values({ name: 'Alice', email: 'alice@example.com' }),06db.insert(users).values({ name: 'Bob', email: 'bob@example.com' }),07db.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 对象存储,处理文件上传的场景。