Seeder 和 Migration 的邊界
Migration 管 schema(table 結構、欄位、index、constraint)。Migration 一定要跑,而且要幂等(跑多次結果一樣)。
Seeder 管資料(初始化必要的資料列)。Seeder 不一定每次都跑,執行時機和目的不同。
混用的常見錯誤:
// ❌ 在 migration 裡直接插資料
module.exports = {
up: async (queryInterface) => {
await queryInterface.createTable('roles', { ... });
// Migration 不該管資料
await queryInterface.bulkInsert('roles', [
{ name: 'admin' }, { name: 'user' }
]);
}
};Schema 和資料要分開——migration 失敗可以 rollback schema,但夾在 migration 裡的 INSERT 很難 rollback。
三種 Seeder 類型
類型一:必要初始資料(Essential Seed)
系統啟動後必須存在才能正常運作的資料。不是「好用的預設」,而是「沒有就壞掉」:
- 角色(admin、user、guest)
- 系統設定(feature flag 的預設值、幣別清單、語系清單)
- 超級管理員帳號(第一個使用者)
這類 seeder 要設計成幂等的——跑多次不會有重複資料:
// Sequelize seeder:幂等設計
module.exports = {
up: async (queryInterface) => {
const roles = ['admin', 'moderator', 'user'];
for (const name of roles) {
await queryInterface.sequelize.query(
`INSERT INTO roles (name) VALUES (:name)
ON CONFLICT (name) DO NOTHING`,
{ replacements: { name } }
);
}
},
down: async (queryInterface) => {
await queryInterface.bulkDelete('roles', null, {});
}
};ON CONFLICT DO NOTHING(PostgreSQL)或 INSERT IGNORE(MySQL)讓 seeder 可以安全重跑。
執行時機:跟 migration 同一個 pipeline,在 app 啟動前跑。但要和 migration 分開——migration 先跑建 schema,seeder 再跑填資料。
類型二:測試用假資料(Test Fixture / Factory)
給開發環境和 CI 用的假資料,不應該進生產環境:
- 假的使用者帳號(100 個不同 role 的 user)
- 假的商品、訂單、文章
- 邊界測試資料(email 有特殊字元、名字超長的 user)
這類資料最好用 Factory Pattern 設計,而不是硬寫一批 INSERT:
// Sequelize factory(用 sequelize-mock 或 fishery)
const userFactory = Factory.define<User>('User', ({ sequence }) => ({
name: `User ${sequence}`,
email: `user${sequence}@example.com`,
role: 'user',
createdAt: new Date(),
}));
// 生成 100 個測試用 user
const users = await userFactory.buildList(100);
await User.bulkCreate(users);
// 生成特定條件的 user
const adminUser = await userFactory.create({ role: 'admin' });Factory 的好處是:每次生成的資料都是確定的(不是亂數),但又不需要把 100 個物件的 JSON 存在 codebase 裡。
執行時機:只在開發環境和 CI 跑,不跑在 staging / production。CI pipeline 可以在跑 integration test 前執行。
類型三:生產環境補資料(Production Data Patch)
有時候系統上線後,需要對既有資料做補填(例如:新增一個欄位後要幫舊資料填預設值)。這不是 migration(schema 不變),也不是 seeder(不是初始資料)——它是一個一次性的 data patch。
這類操作最好寫成獨立的 script,而不是放在 seeder 目錄裡:
// scripts/patch-users-missing-role.ts
async function main() {
const usersWithoutRole = await User.findAll({ where: { role: null } });
console.log(`Patching ${usersWithoutRole.length} users...`);
for (const user of usersWithoutRole) {
await user.update({ role: 'user' });
}
console.log('Done.');
}
main().catch(console.error).finally(() => process.exit(0));一次性 script 有幾個好處:可以 dry-run(先 console.log 不真的更新)、可以加 progress bar、失敗了可以從中間重跑(加 checkpoint)。
各框架工具
| 框架 | Factory / Fixture 工具 | 說明 |
|---|---|---|
| Express + Sequelize | fishery、sequelize-mock | fishery 是通用的 factory 工具 |
| FastAPI + SQLAlchemy | factory_boy | Python 生態最成熟的 factory library |
| NestJS + TypeORM | @jorgebodega/typeorm-factory、fishery | |
| Spring Boot | datafaker、Testcontainers | datafaker 生成假資料;Testcontainers 起真實 DB |
| Laravel | ModelFactory(內建) | Laravel 內建 Faker 整合的 Factory,最完整 |
| Rails | factory_bot | Ruby 生態最成熟,幾乎是標準 |
目錄結構建議
database/
├── migrations/ # Schema 變更,按時間戳命名
│ ├── 20240101000000-create-users.ts
│ └── 20240102000000-add-role-to-users.ts
├── seeders/
│ ├── essential/ # 必要初始資料(跑在 prod)
│ │ ├── 01-roles.ts
│ │ └── 02-admin-user.ts
│ └── development/ # 測試假資料(只跑在 dev/CI)
│ ├── 10-fake-users.ts
│ └── 20-fake-posts.ts
└── factories/ # Factory 定義
├── user.factory.ts
└── post.factory.ts
scripts/
└── patches/ # 一次性 data patch
└── 2024-03-01-fill-missing-roles.ts
CI 的 Seeder 執行順序
CI Pipeline:
1. 啟動測試 DB(docker compose 或 Testcontainers)
2. Run migrations
3. Run essential seeders(必要初始資料)
4. Run development seeders(假資料)
5. Run tests
6. Tear down DB
Essential seeder 要先跑,因為測試可能依賴角色、設定等資料存在。Development seeder 後跑,因為它們依賴 essential data(例如 fake user 要關聯到 role)。
