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 + Sequelizefisherysequelize-mockfishery 是通用的 factory 工具
FastAPI + SQLAlchemyfactory_boyPython 生態最成熟的 factory library
NestJS + TypeORM@jorgebodega/typeorm-factory、fishery
Spring BootdatafakerTestcontainersdatafaker 生成假資料;Testcontainers 起真實 DB
LaravelModelFactory(內建)Laravel 內建 Faker 整合的 Factory,最完整
Railsfactory_botRuby 生態最成熟,幾乎是標準

目錄結構建議

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)。


延伸閱讀