sell整合實戰
schema

設計 D1 資料庫綱要(資料表與 migration)

好的資料庫綱要是每個應用的地基——設計一次,之後永遠都能安全地修改它。

2示範資料表
1:N關聯型態
SQLiteSQL 方言
0001+有版本的 migration
insights

我們要設計什麼?

綱要(schema)就是你資料庫的設計藍圖——有哪些資料表、每張表有哪些欄位,以及讓資料保持乾淨的規則。這篇我們會設計一個迷你部落格:使用者(users)撰寫貼文(posts)。你會學到主鍵(primary key)、外鍵(foreign key)、索引(index),以及 NOT NULL/UNIQUE 規則,最後用 migration 管理之後的每一次變更。

D1 底層是 SQLite,所以這裡用的就是樸實的標準 SQL。核心觀念是:一開始把資料表設計好,之後絕不直接手動改線上資料庫——而是為每次變更寫一個小小的 migration 檔,讓你的綱要擁有清楚、有版本紀錄的歷史。

schema綱要設計的旅程

列出你的資料

設計資料表與欄位

加上鍵與索引

寫一個 migration 檔

用 Wrangler 套用

有版本紀錄的 D1 綱要

schema

資料模型:users 與 posts

一位使用者可以發表很多篇貼文,而每篇貼文剛好屬於一位使用者,這就是「一對多」(one-to-many)關聯。我們用外鍵把它們連起來:posts.user_id 指回 users.id。

schemaER 圖:users ↔ posts

發表

USERS

integer

id

PK

自動編號

text

email

UK

唯一登入

text

name

顯示名稱

text

created_at

建立時間

POSTS

integer

id

PK

自動編號

integer

user_id

FK

作者

text

title

不可空白

text

body

內文

text

status

草稿或上線

text

created_at

建立時間

key

主鍵(PK)

id 用來唯一辨識每一列。在 SQLite 裡 INTEGER PRIMARY KEY 會在每次新增時自動填入一個新號碼。

link

外鍵(FK)

posts.user_id 存放擁有者使用者的 id,把兩張表連起來,並讓資料保持一致。

manage_search

索引(index)

在 user_id 上建索引,能讓資料庫直接跳到某位使用者的貼文,而不必把整張表掃過一遍。

block

NOT NULL(不可空白)

email 與 name 是必填——只要這些欄位留空,資料庫就會拒絕那一列。

fingerprint

UNIQUE(唯一)

email 設為 UNIQUE,所以兩位使用者絕不可能用同一個信箱註冊。

account_tree

一對多

魚尾符號(o{)代表一位使用者對應很多貼文——這是應用程式裡最常見的關聯。

school

綱要術語小辭典

以下是你之後設計幾乎每一張資料表都會用到的基本元件。

schema

綱要 schema

你所有資料表、欄位、型別與規則的完整定義——也就是資料庫的藍圖。

data_object

資料型別

SQLite 用 TEXT、INTEGER、REAL、BLOB、NULL 五種。時間戳記建議用 TEXT(ISO 字串)儲存,最單純。

key

主鍵

用來唯一命名每一列的欄位。INTEGER PRIMARY KEY 還會幫你自動遞增編號。

link

外鍵

一個指向另一張表主鍵的欄位,用來表達列與列之間的關聯。

manage_search

索引

一種查找捷徑,能加速某欄位的篩選與 join——代價是寫入會稍微慢一點點。

rule

約束條件

像 NOT NULL、UNIQUE、DEFAULT 這類由資料庫強制執行的規則,讓壞資料根本進不來。

start

DEFAULT 預設值

沒給欄位值時自動帶入的值,例如 status 預設為 'draft'、created_at 預設為現在時間。

update

Migration(綱要遷移)

一個有編號的 SQL 檔,描述一次綱要變更。依序執行後,就替你的資料庫建立了版本歷史。

swap_vert

用 migration 修改綱要

應用程式會長大:你總會需要新增欄位、新表,或新索引。Migration 就是一個小小、有編號的 SQL 檔(0001_...、0002_...),剛好記錄一次變更。Wrangler 會記住哪些檔案已經跑過,所以在每個環境套用都能重現一樣的結果。

黃金守則:一旦某個 migration 已經套用到共用或正式資料庫,就絕對不要再去改那個檔案。要修正或變更,就寫一個新的 migration。這能讓歷史保持誠實,隊友只要跑一次 migrations apply 就能跟上進度。

schemaMigration 工作流程

決定要改什麼

wrangler d1 migrations create

寫 CREATE/ALTER SQL

apply --local 測試

成功?

apply --remote

D1 內有版本的綱要

history

為什麼要用有版本的檔案?

因為 migrations 資料夾會和你的程式碼一起放進 Git。任何人 clone 下來後,只要依序跑這些 migration,就能重建出一模一樣的綱要——不必手動敲 SQL,也不用猜。

construction

一步步動手做

現在把全部串起來:建立資料庫、綁定到 Worker、在 migration 裡定義綱要、套用它、再加第二個 migration 讓綱要演進,最後用 JOIN 跨兩張表查詢。

  1. 建立資料庫

    Wrangler 會印出一組 database_id——複製起來,下一步會用到。

    bash
    npx wrangler d1 create blog-db
  2. 綁定到你的 Worker

    把這段加進 wrangler.jsonc,讓 Worker 能用 env.DB 存取這個資料庫。

    json
    {
      "d1_databases": [
        {
          "binding": "DB",
          "database_name": "blog-db",
          "database_id": "<paste-your-id-here>"
        }
      ]
    }
  3. 建立第一個 migration

    這會建立一個 migrations/ 資料夾,以及一個空的 0001_create_users_and_posts.sql 檔。

    bash
    npx wrangler d1 migrations create blog-db create_users_and_posts
  4. 寫下綱要 SQL

    打開剛產生的檔案,把兩張表連同它們的鍵、約束與索引都定義好。

    sql
    -- migrations/0001_create_users_and_posts.sql
    -- Enforce foreign keys (SQLite leaves this off by default)
    PRAGMA foreign_keys = ON;
    
    CREATE TABLE users (
      id         INTEGER PRIMARY KEY,
      email      TEXT NOT NULL UNIQUE,
      name       TEXT NOT NULL,
      created_at TEXT NOT NULL DEFAULT (datetime('now'))
    );
    
    CREATE TABLE posts (
      id         INTEGER PRIMARY KEY,
      user_id    INTEGER NOT NULL,
      title      TEXT NOT NULL,
      body       TEXT,
      status     TEXT NOT NULL DEFAULT 'draft',
      created_at TEXT NOT NULL DEFAULT (datetime('now')),
      FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
    );
    
    -- Index the columns we filter and join on
    CREATE INDEX idx_posts_user_id ON posts (user_id);
    CREATE INDEX idx_posts_status  ON posts (status);
  5. 先套用到本機,再套用到遠端

    先對本機副本測試;確認沒問題後,再套用到雲端上真正的資料庫。

    bash
    # Build the schema on the local test DB
    npx wrangler d1 migrations apply blog-db --local
    
    # See what is still pending on production
    npx wrangler d1 migrations list blog-db --remote
    
    # Apply to the real cloud database
    npx wrangler d1 migrations apply blog-db --remote
  6. 之後讓綱要演進

    需要新欄位?建立第二個 migration,而不是去改第一個——接著用同樣方式套用。

    bash
    npx wrangler d1 migrations create blog-db add_published_at_to_posts
    
    # In migrations/0002_add_published_at_to_posts.sql:
    #   ALTER TABLE posts ADD COLUMN published_at TEXT;
    
    npx wrangler d1 migrations apply blog-db --local
    npx wrangler d1 migrations apply blog-db --remote
  7. 從 Worker 跨表查詢

    prepare() 建立安全查詢,bind() 填入 ? 佔位符(擋掉 SQL 注入),JOIN 則把每篇貼文和它的作者一起拉出來。

    js
    export default {
      async fetch(request, env) {
        const url = new URL(request.url);
        const userId = url.searchParams.get("user_id") ?? "1";
    
        // Join each post to its author, newest first.
        const { results } = await env.DB
          .prepare(
            `SELECT posts.id, posts.title, posts.status, users.name AS author
             FROM posts
             JOIN users ON users.id = posts.user_id
             WHERE posts.user_id = ?
             ORDER BY posts.created_at DESC`
          )
          .bind(userId)
          .all();
    
        return Response.json(results);
      },
    };
  8. 部署上線

    發布這個 Worker——你那個有綱要撐腰的 API 就上線了。

    bash
    npx wrangler deploy

塞一點資料來測試

bash插入範例資料
npx wrangler d1 execute blog-db --remote --command "INSERT INTO users (email, name) VALUES ('mei@example.com', 'Mei');"

npx wrangler d1 execute blog-db --remote --command "INSERT INTO posts (user_id, title, status) VALUES (1, 'Hello D1', 'live');"
tips_and_updates

常見陷阱與小提示

warning

絕不要改已套用的 migration

一旦 0001 已經在共用資料庫跑過,再去改它也不會重跑,隊友的資料庫就會慢慢和你的不同步。要改變更,永遠加一個新的有編號 migration。

  • 用 PRAGMA foreign_keys = ON; 開啟外鍵約束,否則 SQLite 會忽略 FK 規則。
  • 為每個外鍵、以及任何常用來篩選或排序的欄位都建立索引。
  • SQLite 的 ALTER TABLE 能新增欄位、改名,但不容易刪欄位——欄位請事先想清楚再設計。
  • 新增欄位時優先用 NOT NULL 搭配 DEFAULT,這樣既有的列才不會變不合法。
  • 每個 migration 都先用 --local 測過再 --remote;本機副本又快又可隨時丟掉重建。
  • 把 migrations/ 資料夾放進 Git,整段綱要歷史就會跟著你的程式碼一起走。
savings

索引能省下讀取列數

D1 是依讀取與寫入的列數計費。好的索引能讓查詢只碰到真正需要的列,而不是把整張表掃一遍——回應更快,帳單也更小。