Chapter 08

数据库与 ORM 集成

Drizzle ORM 类型安全 SQL、Cloudflare D1 边缘数据库与博客系统完整 API 实战

Drizzle ORM 简介

Drizzle ORM 是与 Hono 最匹配的 TypeScript ORM——轻量、类型安全、支持边缘运行时(无 Node.js 依赖)。它采用"SQL-first"设计哲学:写 Drizzle 代码就像写 SQL,但享有完整的 TypeScript 类型推断。

特性Drizzle ORMPrisma
边缘运行时支持完整支持部分支持(需 Accelerate)
包大小(运行时)~30KB~1.5MB+
查询风格SQL-like,直观类对象,抽象更高
Cloudflare D1原生支持需要第三方适配
Schema 定义TypeScript 代码.prisma 文件

安装与配置

# 安装 Drizzle(SQLite / D1 适配器)
bun add drizzle-orm
bun add -D drizzle-kit

Schema 定义

// src/db/schema.ts
import { sqliteTable, text, integer, index } from 'drizzle-orm/sqlite-core'
import { sql } from 'drizzle-orm'

// 用户表
export const users = sqliteTable('users', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  name: text('name').notNull(),
  email: text('email').notNull().unique(),
  passwordHash: text('password_hash').notNull(),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull().default(sql`(unixepoch())`),
}, (table) => ({
  emailIdx: index('email_idx').on(table.email),
}))

// 文章表
export const posts = sqliteTable('posts', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  title: text('title').notNull(),
  slug: text('slug').notNull().unique(),
  content: text('content').notNull(),
  published: integer('published', { mode: 'boolean' }).notNull().default(false),
  authorId: text('author_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
  createdAt: integer('created_at', { mode: 'timestamp' })
    .notNull().default(sql`(unixepoch())`),
  updatedAt: integer('updated_at', { mode: 'timestamp' })
    .notNull().default(sql`(unixepoch())`),
})

// 标签表
export const tags = sqliteTable('tags', {
  id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
  name: text('name').notNull().unique(),
})

// 文章-标签关联表(多对多)
export const postTags = sqliteTable('post_tags', {
  postId: text('post_id').notNull().references(() => posts.id, { onDelete: 'cascade' }),
  tagId: text('tag_id').notNull().references(() => tags.id, { onDelete: 'cascade' }),
})

Cloudflare D1 + Drizzle 集成

// src/db/index.ts — D1 适配器
import { drizzle } from 'drizzle-orm/d1'
import * as schema from './schema'

export function createDb(d1: D1Database) {
  return drizzle(d1, { schema })
}

export type DB = ReturnType<typeof createDb>
// src/index.ts — 主入口
import { Hono } from 'hono'
import { createDb } from './db'
import postsRouter from './routes/posts'

type Env = { Bindings: { DB: D1Database } }

const app = new Hono<Env>()

// 将 db 实例注入到 context
app.use(async (c, next) => {
  c.set('db', createDb(c.env.DB))
  await next()
})

app.route('/api/posts', postsRouter)

export default app

博客系统:完整 API 实战

// src/routes/posts.ts — 文章 CRUD API
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { eq, desc, like, and, inArray } from 'drizzle-orm'
import { posts, tags, postTags } from '../db/schema'

const postsRouter = new Hono()

// 列表(分页 + 搜索 + 标签过滤)
postsRouter.get(
  '/',
  zValidator('query', z.object({
    page: z.coerce.number().int().min(1).default(1),
    limit: z.coerce.number().int().max(50).default(10),
    search: z.string().optional(),
    published: z.coerce.boolean().optional(),
  })),
  async (c) => {
    const db = c.get('db')
    const { page, limit, search, published } = c.req.valid('query')
    const offset = (page - 1) * limit

    const conditions = []
    if (search) conditions.push(like(posts.title, `%${search}%`))
    if (published !== undefined) conditions.push(eq(posts.published, published))

    const results = await db
      .select({
        id: posts.id, title: posts.title, slug: posts.slug,
        published: posts.published, createdAt: posts.createdAt,
      })
      .from(posts)
      .where(and(...conditions))
      .orderBy(desc(posts.createdAt))
      .limit(limit)
      .offset(offset)

    return c.json({ posts: results, page, limit })
  }
)

// 详情(含标签)
postsRouter.get('/:id', async (c) => {
  const db = c.get('db')
  const id = c.req.param('id')

  const [post] = await db.select().from(posts).where(eq(posts.id, id))
  if (!post) return c.json({ error: 'Post not found' }, 404)

  // 查询关联标签
  const postTagsResult = await db
    .select({ name: tags.name })
    .from(postTags)
    .innerJoin(tags, eq(postTags.tagId, tags.id))
    .where(eq(postTags.postId, id))

  return c.json({ ...post, tags: postTagsResult.map(t => t.name) })
})

// 创建文章
postsRouter.post(
  '/',
  zValidator('json', z.object({
    title: z.string().min(1).max(200),
    content: z.string().min(1),
    published: z.boolean().default(false),
    tagNames: z.array(z.string()).default([]),
  })),
  async (c) => {
    const db = c.get('db')
    const { title, content, published, tagNames } = c.req.valid('json')
    const authorId = c.get('userId') ?? 'anonymous'

    // 生成 slug
    const slug = title.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-')

    // 插入文章
    const [post] = await db.insert(posts).values({ title, slug, content, published, authorId }).returning()

    // 处理标签(upsert)
    if (tagNames.length > 0) {
      const tagResults = await Promise.all(
        tagNames.map(name =>
          db.insert(tags).values({ name }).onConflictDoUpdate({
            target: tags.name, set: { name }
          }).returning()
        )
      )
      await db.insert(postTags).values(
        tagResults.flat().map(tag => ({ postId: post.id, tagId: tag.id }))
      )
    }

    return c.json({ ...post, tags: tagNames }, 201)
  }
)

export default postsRouter

Turso:全球分布式 SQLite

Turso 是基于 libSQL(SQLite 分支)的全球分布式数据库,可以在全球多个区域创建副本,与 Drizzle 完美集成:

# 安装 Turso 客户端
bun add @libsql/client drizzle-orm
// src/db/turso.ts
import { createClient } from '@libsql/client'
import { drizzle } from 'drizzle-orm/libsql'
import * as schema from './schema'

export function createTursoDb(url: string, authToken: string) {
  const client = createClient({ url, authToken })
  return drizzle(client, { schema })
}

// 使用(Node.js / Bun 环境)
const db = createTursoDb(
  process.env.TURSO_DATABASE_URL!,
  process.env.TURSO_AUTH_TOKEN!,
)

连接池策略:在 Edge 环境(Cloudflare Workers / Vercel Edge)中,每次请求可能由不同的 Isolate 处理,无法维持持久连接池。应使用 HTTP-based 客户端(如 Turso libSQL HTTP、Neon HTTP)而非 TCP 连接池。在传统服务器(Node.js / Bun)上,可以使用连接池(Prisma 默认 5 个连接)。