iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
佛心分享-IT 人自學之術

出發吧!後端菜鳥:30 天的後端學習紀錄系列 第 18 篇

Day 18|資料庫結構也要版本管理:認識 Migration

  • 分享至 

  • xImage
  •  

前言

前面已經開始使用 PostgreSQL,也透過 SQL 學會建立資料表、寫入資料與查詢資料,接著又認識 ORM,知道可以透過 EntitySchema 將資料庫中的 Table 映射成程式可以使用的模型。

不過專案不會永遠維持相同的資料庫結構。隨著需求增加,原本只有 id、title、content 的 notes Table,可能需要加入 status、created_at 或其他欄位。這代表除了資料會持續增加之外,資料庫的 Schema 也會跟著專案一起改變。

最直接的方式,是每次需要修改資料庫時,直接連進 PostgreSQL 執行 SQL。例如:

ALTER TABLE notes
ADD COLUMN status VARCHAR(20);

這樣確實可以把欄位加進去,但問題是這次修改只發生在當下的資料庫裡。如果之後需要重新建立一個環境,或者其他開發者需要同步相同的結構,就必須另外確認「之前到底改過什麼」。

當專案規模變大後,本機、測試環境與 Production 也可能因為不同人執行過不同 SQL,而逐漸產生差異。

因此真正需要解決的,不是「怎麼修改資料庫」,而是資料庫結構的變更要怎麼被記錄與管理。

Schema 也會跟著專案改變

Schema 可以先理解成資料庫的結構,例如 Table 有哪些欄位、欄位使用什麼型別、哪些欄位是 Primary Key、哪些欄位有預設值,以及不同 Table 之間存在什麼關係。

例如原本的 notes:

notes
├── id
├── title
└── content

當需求增加筆記狀態後,就變成:

notes
├── id
├── title
├── content
└── status

這就是一次 Schema Change。

如果每次 Schema Change 都直接手動修改資料庫,時間久了就很難知道資料庫經歷過哪些變化,也無法保證每個環境都經過相同的修改。因此,專案需要一種方式把這些變更保存下來,Migration 就是在這種情況下出現的。

Migration 是什麼?

Migration 可以理解成:把資料庫 Schema 的變更記錄成一個個可以執行的檔案。

例如一個專案可能會有:

001-create-users
002-create-notes
003-add-status-to-notes
004-add-created-at-to-notes

每一個 Migration 都代表一次資料庫結構的變更,並且按照順序保存下來。這樣資料庫就不只是有「現在的結構」,還可以知道「這個結構是怎麼一步一步變過來的」。

Migration 通常會放在專案的 migrations 目錄:

project/
├── src/
│   ├── entities/
│   ├── migrations/
│   │   ├── 001-create-users.ts
│   │   ├── 002-create-notes.ts
│   │   └── 003-add-status-to-notes.ts
│   └── data-source.ts
└── package.json

這些檔案也會跟著專案一起進行版本管理,所以當其他開發者取得最新程式碼後,就可以依照相同的 Migration 更新自己的資料庫,而不需要靠人工確認過去做過哪些修改。

從 EntitySchema 開始

前一天已經使用 EntitySchema 定義 notes Table,因此這次就從原本的 EntitySchema 直接開始。

假設目前的 NoteEntity 是:

import { EntitySchema } from "typeorm";

export const NoteEntity = new EntitySchema({
  name: "Note",
  tableName: "notes",
  columns: {
    id: {
      type: Number,
      primary: true,
      generated: true,
    },
    title: {
      type: String,
    },
    content: {
      type: String,
    },
  },
});

這段程式碼描述的是目前 notes Table 的結構,而今天的需求是替筆記增加一個 status 欄位,預設值為 draft。

因此先修改 EntitySchema:

import { EntitySchema } from "typeorm";

export const NoteEntity = new EntitySchema({
  name: "Note",
  tableName: "notes",
  columns: {
    id: {
      type: Number,
      primary: true,
      generated: true,
    },
    title: {
      type: String,
    },
    content: {
      type: String,
    },
    status: {
      type: String,
      default: "draft",
    },
  },
});

這裡新增的是:

status: {
  type: String,
  default: "draft",
},

代表我們希望 notes Table 未來多一個 status 欄位,而且預設值是 "draft"。

不過這個時候有一件事情很重要:我們只是修改了 EntitySchema,資料庫本身還沒有改變。

現在的狀態是:

EntitySchema
id
title
content
status

        ↓

Database
id
title
content

兩邊出現差異後,就需要透過 Migration 把這個變更套用到資料庫。

產生 Migration

修改完 EntitySchema 後,可以讓 TypeORM 比較目前 Entity 定義與資料庫實際 Schema 的差異,並產生 Migration。

執行:

npm run typeorm migration:generate -- -d src/data-source.ts src/migrations/AddStatusToNotes

TypeORM 會找出 EntitySchema 與目前資料庫之間的差異,接著產生一個新的 Migration 檔案。

例如:

src/
├── entities/
│   └── Note.ts
├── migrations/
│   └── 1750000000000-AddStatusToNotes.ts
└── data-source.ts

這時候先不要急著執行 Migration,而是先看看它產生了什麼內容。

你可能會看到類似:

async up(queryRunner: QueryRunner): Promise<void> {
  await queryRunner.query(
    `ALTER TABLE "notes" ADD "status" character varying NOT NULL DEFAULT 'draft'`,
  );
}

async down(queryRunner: QueryRunner): Promise<void> {
  await queryRunner.query(
    `ALTER TABLE "notes" DROP COLUMN "status"`,
  );
}

這裡先不用深入 Migration 檔案裡每個 TypeORM API 的細節,只要先理解 up 和 down 的概念。

up 代表套用這次變更,也就是新增 status;down 則代表回復這次變更,也就是將 status 移除。

因此可以理解成:

up
→ 資料庫往新的 Schema 前進

down
→ 回到這次變更之前的 Schema

執行 Migration

確認產生的 Migration 沒有問題後,就可以真正把它套用到資料庫。

執行:

npm run typeorm migration:run -- -d src/data-source.ts

TypeORM 會執行目前尚未套用的 Migration,資料庫的 notes Table 就會從:

notes
├── id
├── title
└── content

變成:

notes
├── id
├── title
├── content
└── status

這時候 EntitySchema 和資料庫的 Schema 就重新一致了。

整個過程可以整理成:

修改 EntitySchema
      ↓
產生 Migration
      ↓
確認 Migration
      ↓
執行 Migration
      ↓
更新資料庫 Schema

這就是 Migration 最基本的使用流程。

怎麼知道哪些 Migration 已經執行?

專案開發一段時間之後,Migration 會越來越多,因此還需要知道目前資料庫到底執行到哪裡。

可以使用:

npm run typeorm migration:show -- -d src/data-source.ts

TypeORM 會顯示目前 Migration 的執行狀態,例如:

[X] 1750000000000-AddStatusToNotes

[X] 代表這個 Migration 已經執行。

當專案有很多 Migration 時,就可以透過這個方式確認目前資料庫是否已經套用最新的 Schema Change。

如果需要回復呢?

如果剛才的 Migration 在開發階段發現有問題,也可以把最近一次變更回復。

執行:

npm run typeorm migration:revert -- -d src/data-source.ts

TypeORM 會執行最近一次 Migration 的 down,也就是將剛才新增的 status 欄位移除。

因此完整的操作就會變成:

migration:generate
        ↓
產生 Migration
        ↓
migration:run
        ↓
套用 Schema Change
        ↓
migration:revert
        ↓
回復最近一次 Schema Change

這也是為什麼 Migration 不只是單純記錄「資料庫改過什麼」,而是把每次變更整理成可以實際執行與回復的內容。

為什麼 Production 不應該直接手動修改?

到了 Production,Migration 的價值會更加明顯。

假設今天需要新增 status,與其直接登入 Production 執行:

ALTER TABLE notes
ADD COLUMN status VARCHAR(20);

比較好的方式,是先修改 EntitySchema,產生 Migration,在開發環境確認沒有問題,再經過測試環境驗證,最後將相同的 Migration 套用到 Production。

流程會比較接近:

修改 EntitySchema
      ↓
產生 Migration
      ↓
本機測試
      ↓
測試環境驗證
      ↓
Production 執行 Migration

如此一來,每個環境執行的都是同一份 Schema Change,而不是每個人各自登入資料庫修改。

這也能讓資料庫結構的變更跟著專案一起保存。過了一段時間之後,如果想知道某個欄位是什麼時候加入的、為什麼會出現在資料庫裡,只需要回頭查看對應的 Migration。

Migration 對團隊開發的價值

Migration 真正重要的地方,是讓資料庫 Schema 的變化變得有紀錄、有順序,而且可以重複套用。

例如一個專案開發一段時間後,可能累積了:

001-create-users
002-create-notes
003-add-status-to-notes
004-add-created-at-to-notes
005-add-category-to-notes

當新的開發者加入專案,或需要建立新的開發環境時,就可以按照 Migration 的順序更新資料庫,不需要依賴某個人的記憶,也不需要重新整理一份「以前曾經手動執行過哪些 SQL」。

Migration 檔案也會跟著專案一起進行版本管理,因此資料庫結構的變化也能成為程式開發流程的一部分。

換句話說,EntitySchema 描述的是現在希望資料庫長什麼樣子,Migration 記錄的則是資料庫要如何從原本的結構一步一步變成現在的樣子。

小結

資料庫的 Schema 不會永遠維持不變,隨著專案持續開發,資料表、欄位、Index 與關聯都可能跟著需求調整。如果每次都直接登入資料庫手動修改,時間久了很容易讓不同環境產生差異,也很難追蹤資料庫究竟經歷過哪些變更。

Migration 就是用來處理這件事。當 EntitySchema 發生變化時,可以先產生 Migration,再確認它實際要執行的 Schema Change,最後透過 migration:run 套用到資料庫,需要回復時則可以使用 migration:revert。

如此一來,資料庫就不只是保存「現在的結構」,也留下了結構一路演進的過程。


上一篇
Day 17|讓資料庫操作變得更簡單:ORM
系列文
出發吧!後端菜鳥:30 天的後端學習紀錄 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言