【Cloudflare Workers 全端架構師之路 04】數據篇:D1 資料庫設計模式與效能優化
01. 前言:為什麼 Edge 需要 SQLite?
在傳統 Serverless (AWS Lambda) 開發中,連接關聯式資料庫 (MySQL/PostgreSQL) 一直是個惡夢。 因為 Lambda 是無狀態且會隨流量暴增 (Scale Out) 的,當 1000 個請求同時進來,會瞬間建立 1000 個資料庫連線,直接把 RDS 打掛。你必須額外花錢買 RDS Proxy 來解決這個問題。
Cloudflare D1 的出現解決了這個痛點。
D1 是建立在 SQLite 之上的 Serverless 資料庫。 它沒有傳統的 TCP 連線問題,因為它是透過 HTTP/RPC 協議與 Workers 通訊的。它天生就是為了高併發、短壽命的 Workers 環境設計的。
架構師觀點: 不要小看 SQLite。它不僅是手機裡的資料庫,在 D1 的架構下,它具備了自動備份、時間點恢復 (PITR) 以及讀取複寫 (Read Replication) 的企業級能力。
02. 架構比較:D1 vs. AWS RDS
| 特性 | Cloudflare D1 | AWS RDS (PostgreSQL/MySQL) |
|---|---|---|
| 底層引擎 | SQLite | PostgreSQL / MySQL |
| 連線管理 | 無須管理 (HTTP/RPC) | 需管理 Connection Pool (或使用 RDS Proxy) |
| 部署位置 | 全球邊緣 (Primary 在特定區域,Replicas 自動散佈) | 指定區域 (VPC 內) |
| 冷啟動延遲 | 極低 | 需等待 TCP Handshake 與驗證 |
| 計費模式 | 讀寫列數 (Row reads/writes) + 儲存量 | 實例小時費 (Instance hour) + 流量費 |
03. 環境建置:D1 + Drizzle ORM
為什麼選擇 Drizzle ORM? 相比於 Prisma (引擎肥大、冷啟動慢),Drizzle 幾乎是 Zero-runtime overhead,且產生的 SQL 非常乾淨。它是目前 Serverless 環境的標準配備。
步驟 1: 建立資料庫
npx wrangler d1 create my-blog-db
將輸出的 ID 填入 wrangler.toml:
[[d1_databases]]
binding = "DB"
database_name = "my-blog-db"
database_id = "你的_DATABASE_ID"
步驟 2: 安裝套件
npm install drizzle-orm
npm install -D drizzle-kit
04. 實作:關聯式 Schema 設計
我們來設計一個典型的部落格系統:使用者 (Users)、文章 (Posts) 與 評論 (Comments)。
建立 src/schema.ts:
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { relations } from 'drizzle-orm';
// 1. Users Table
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: integer('created_at', { mode: 'timestamp' }).$defaultFn(() => new Date()),
});
// 2. Posts Table
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
authorId: integer('author_id').notNull().references(() => users.id), // Foreign Key
title: text('title').notNull(),
content: text('content').notNull(),
published: integer('published', { mode: 'boolean' }).default(false),
});
// 3. 定義關聯 (Application Level)
// 這讓 Drizzle 知道如何執行 query.users.findMany({ with: { posts: true } })
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
}));
步驟 3: 執行 Migration
建立 drizzle.config.ts 並執行:
npx drizzle-kit generate
npx wrangler d1 migrations apply my-blog-db --local # 本地測試
05. 深度實作:JOIN 查詢與 Transaction
在 API 開發中,最考驗效能的就是 N+1 問題與資料一致性。讓我們看看如何在 Worker 中優雅地處理。
修改 src/index.ts:
import { Hono } from 'hono'
import { drizzle } from 'drizzle-orm/d1'
import { users, posts } from './schema'
import * as schema from './schema'
import { eq } from 'drizzle-orm'
type Bindings = { DB: D1Database }
const app = new Hono<{ Bindings: Bindings }>()
// GET: 取得使用者及其所有文章 (解決 N+1 問題)
app.get('/users/:id/posts', async (c) => {
const db = drizzle(c.env.DB, { schema }); // 注入 schema 以啟用關聯查詢
const id = Number(c.req.param('id'));
// Drizzle 的 query builder 會自動優化成單條 SQL 或高效的 JOIN
const result = await db.query.users.findFirst({
where: eq(users.id, id),
with: {
posts: {
// 只撈出已發布的文章
where: eq(posts.published, true),
columns: { content: false } // 優化:不撈取大欄位
}
}
});
if (!result) return c.json({ error: 'User not found' }, 404);
return c.json(result);
})
// POST: 批量發布文章 (Transaction 範例)
app.post('/posts/batch', async (c) => {
const db = drizzle(c.env.DB);
const body = await c.req.json();
const { authorId, articles } = body;
try {
// 交易保證:要嘛全成功,要嘛全失敗
const result = await db.transaction(async (tx) => {
const insertedPosts = [];
for (const article of articles) {
const res = await tx.insert(posts).values({
authorId,
title: article.title,
content: article.content,
published: true
}).returning({ id: posts.id });
insertedPosts.push(res[0]);
}
return insertedPosts;
});
return c.json({ success: true, data: result });
} catch (e) {
return c.json({ error: 'Transaction failed', details: String(e) }, 500);
}
})
export default app
06. 效能優化與注意事項
-
索引 (Indexes): D1 是 SQLite,所以索引至關重要。記得在
schema.ts中對常查詢的欄位 (如email,author_id) 加上索引。import { index } from 'drizzle-orm/sqlite-core'; // 在 table 定義最後加上 // (example) }, (table) => ({ emailIdx: index('email_idx').on(table.email) })); -
併發寫入限制: D1 雖然解決了連線問題,但 SQLite 本質上是 單一寫入者 (Single Writer)。
- 讀取 (Read):可以無限水平擴展 (透過 Read Replicas)。
- 寫入 (Write):會被序列化 (Serialized)。
- 對策:如果你的應用是「超高頻寫入」(如 IoT Sensor log),D1 可能會遇到瓶頸。這時候應該改用 Workers Analytics Engine 或透過 Queues 進行批次寫入 (Batch Write)。
07. 小結與下一步
我們成功構建了一個具備 關聯查詢 與 交易保護 的 Edge 資料庫層。 D1 讓我們不再需要煩惱 VPC、Connection Pool 或是昂貴的 RDS 帳單,同時保持了 SQL 的強大功能。
現在資料有了,但如果使用者要上傳頭像或文章配圖呢?把圖片存進 D1 是絕對錯誤的做法 (BLOB 會拖慢資料庫)。
在下一篇 Part 5: 儲存篇,我們將引入 R2 Storage。 這不僅僅是 S3 的替代品,重點在於它的 零出口流量費 (Zero Egress Fee)。我們將教你如何省下大筆 AWS 帳單,並實作安全的 Presigned URL 上傳機制。

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。
系列文章目錄
- 【Cloudflare Workers 全端架構師之路 02】入門篇:開發環境與 Hono 框架最佳實踐
- 【Cloudflare Workers 全端架構師之路 03】設定篇:KV 快取架構與分層策略
- 【Cloudflare Workers 全端架構師之路 04】數據篇:D1 資料庫設計模式與效能優化 (本文)
- 【Cloudflare Workers 全端架構師之路 05】儲存篇:R2 檔案處理與零流量費架構
- 【Cloudflare Workers 全端架構師之路 06】安全篇:守門員 Middleware、CORS 與 JWT 驗證