前面已經開始使用 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 可以先理解成資料庫的結構,例如 Table 有哪些欄位、欄位使用什麼型別、哪些欄位是 Primary Key、哪些欄位有預設值,以及不同 Table 之間存在什麼關係。
例如原本的 notes:
notes
├── id
├── title
└── content
當需求增加筆記狀態後,就變成:
notes
├── id
├── title
├── content
└── status
這就是一次 Schema Change。
如果每次 Schema Change 都直接手動修改資料庫,時間久了就很難知道資料庫經歷過哪些變化,也無法保證每個環境都經過相同的修改。因此,專案需要一種方式把這些變更保存下來,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 定義 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 把這個變更套用到資料庫。
修改完 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 沒有問題後,就可以真正把它套用到資料庫。
執行:
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 會越來越多,因此還需要知道目前資料庫到底執行到哪裡。
可以使用:
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,Migration 的價值會更加明顯。
假設今天需要新增 status,與其直接登入 Production 執行:
ALTER TABLE notes
ADD COLUMN status VARCHAR(20);
比較好的方式,是先修改 EntitySchema,產生 Migration,在開發環境確認沒有問題,再經過測試環境驗證,最後將相同的 Migration 套用到 Production。
流程會比較接近:
修改 EntitySchema
↓
產生 Migration
↓
本機測試
↓
測試環境驗證
↓
Production 執行 Migration
如此一來,每個環境執行的都是同一份 Schema Change,而不是每個人各自登入資料庫修改。
這也能讓資料庫結構的變更跟著專案一起保存。過了一段時間之後,如果想知道某個欄位是什麼時候加入的、為什麼會出現在資料庫裡,只需要回頭查看對應的 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。
如此一來,資料庫就不只是保存「現在的結構」,也留下了結構一路演進的過程。