๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
pnpm migrate ๋ช…๋ น์–ด๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. Entity ์ •์˜๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ž๋™์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•˜๊ณ , ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ผ๊ด€๋˜๊ฒŒ ์ ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๊ธฐ๋ณธ ๊ฐœ๋…

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ์„ ๋ฒ„์ „ ๊ด€๋ฆฌํ•˜๋Š” ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค:
  • Entity ๊ธฐ๋ฐ˜: Entity ์ •์˜์—์„œ ์ž๋™์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
  • ์ˆœ์ฐจ ์‹คํ–‰: ์ƒ์„ฑ ์ˆœ์„œ๋Œ€๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ์šฉ
  • ๋‹ค์ค‘ DB ์ง€์›: ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋™์‹œ์— ๊ด€๋ฆฌ
  • ์ž๋™ ๊ฐ์ง€: Entity ๋ณ€๊ฒฝ์‚ฌํ•ญ ์ž๋™ ๊ฐ์ง€ ๋ฐ ์ฝ”๋“œ ์ƒ์„ฑ

๋ช…๋ น์–ด

status - ์ƒํƒœ ํ™•์ธ

ํ˜„์žฌ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
pnpm migrate status
์ถœ๋ ฅ ์˜ˆ์‹œ:
Development Master: โœ“ Up to date (v20240115_143022)
Testing: โš  2 pending migrations
Production: โœ“ Up to date (v20240115_143022)

Prepared migrations:
  โ€ข 20240116_101530_add_user_profile
  โ€ข 20240116_102045_create_posts_table
์ƒํƒœ ์ข…๋ฅ˜:
  • โœ“ Up to date: ๋ชจ๋“  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ์šฉ ์™„๋ฃŒ
  • โš  N pending: N๊ฐœ์˜ ๋ฏธ์ ์šฉ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
  • โŒ Error: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ

run - ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์‹คํ–‰

๋Œ€๊ธฐ ์ค‘์ธ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ๋ชจ๋‘ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค.
pnpm migrate run
์‹คํ–‰ ๊ณผ์ •:
Running migrations...

Development Master:
  โœ“ 20240116_101530_add_user_profile (0.2s)
  โœ“ 20240116_102045_create_posts_table (0.3s)

Testing:
  โœ“ 20240116_101530_add_user_profile (0.2s)
  โœ“ 20240116_102045_create_posts_table (0.3s)

All migrations completed successfully!
์ž๋™ ์ƒ์„ฑ ๋ฐ ์ ์šฉ:
  • Entity ๋ณ€๊ฒฝ์‚ฌํ•ญ์„ ๊ฐ์ง€
  • ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ฝ”๋“œ ์ž๋™ ์ƒ์„ฑ
  • ๋ชจ๋“  ๋Œ€์ƒ DB์— ์ˆœ์ฐจ ์ ์šฉ
Sonamu๋Š” Entity ์ •์˜๋ฅผ ๋ถ„์„ํ•˜์—ฌ ํ•„์š”ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ž๋™์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜๋™์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ ์ž‘์„ฑํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ

์ž๋™ ์ƒ์„ฑ

Entity๋ฅผ ์ˆ˜์ •ํ•˜๋ฉด Sonamu๊ฐ€ ์ž๋™์œผ๋กœ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.
user.entity.ts
// Entity ์ •์˜ ๋ณ€๊ฒฝ
const UserEntity = {
  properties: [
    {
      name: "email",
      type: "string",
      length: 255,
    },
    // ์ƒˆ ํ•„๋“œ ์ถ”๊ฐ€
    {
      name: "phone",
      type: "string",
      length: 20,
      nullable: true,
    },
  ],
};
# ์ƒํƒœ ํ™•์ธ
pnpm migrate status

# ์ถœ๋ ฅ:
# Prepared migrations:
#   โ€ข 20240116_103045_add_phone_to_users

# ์ ์šฉ
pnpm migrate run

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ

์ƒ์„ฑ๋œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์€ src/migrations/ ๋””๋ ‰ํ† ๋ฆฌ์— ์ €์žฅ๋ฉ๋‹ˆ๋‹ค.
src/migrations/20240116_103045_add_phone_to_users.ts
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
  await knex.schema.alterTable("users", (table) => {
    table.string("phone", 20).nullable();
  });
}

export async function down(knex: Knex): Promise<void> {
  await knex.schema.alterTable("users", (table) => {
    table.dropColumn("phone");
  });
}
๊ตฌ์กฐ:
  • up(): ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ์šฉ (ํ…Œ์ด๋ธ” ์ƒ์„ฑ, ์ปฌ๋Ÿผ ์ถ”๊ฐ€ ๋“ฑ)
  • down(): ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋กค๋ฐฑ (๋ณ€๊ฒฝ์‚ฌํ•ญ ๋˜๋Œ๋ฆฌ๊ธฐ)

์ง€์›ํ•˜๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ

Sonamu๊ฐ€ ์ž๋™์œผ๋กœ ๊ฐ์ง€ํ•˜๊ณ  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์„ ์ƒ์„ฑํ•˜๋Š” ๋ณ€๊ฒฝ์‚ฌํ•ญ:
๋ณ€๊ฒฝ ํƒ€์ž…์„ค๋ช…์˜ˆ์‹œ
ํ…Œ์ด๋ธ” ์ƒ์„ฑ์ƒˆ Entity ์ถ”๊ฐ€PostEntity ์ƒ์„ฑ
์ปฌ๋Ÿผ ์ถ”๊ฐ€์ƒˆ ํ”„๋กœํผํ‹ฐ ์ถ”๊ฐ€phone ํ•„๋“œ ์ถ”๊ฐ€
์ปฌ๋Ÿผ ์ˆ˜์ •ํƒ€์ž…/๊ธธ์ด ๋ณ€๊ฒฝvarchar(100) โ†’ varchar(255)
์ปฌ๋Ÿผ ์‚ญ์ œํ”„๋กœํผํ‹ฐ ์ œ๊ฑฐdeprecated_field ์‚ญ์ œ
์ธ๋ฑ์Šค ์ถ”๊ฐ€์ธ๋ฑ์Šค ์ •์˜indexes ๋ฐฐ์—ด
์™ธ๋ž˜ํ‚ค ์ถ”๊ฐ€๊ด€๊ณ„ ์ •์˜belongsTo ๊ด€๊ณ„

๋‹ค์ค‘ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ด€๋ฆฌ

Sonamu๋Š” ์—ฌ๋Ÿฌ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋ฅผ ๋™์‹œ์— ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ตฌ์„ฑ

sonamu.config.ts
export default {
  database: {
    // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ - ๋งˆ์Šคํ„ฐ
    development_master: {
      client: "mysql2",
      connection: {
        host: "localhost",
        database: "myapp_dev",
        user: "root",
        password: "password",
      },
    },
    // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ - ์Šฌ๋ ˆ์ด๋ธŒ (์ฝ๊ธฐ ์ „์šฉ)
    development_slave: {
      // ์Šฌ๋ ˆ์ด๋ธŒ๋Š” ์ž๋™์œผ๋กœ ๋ฌด์‹œ๋จ
    },
    // ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ
    test: {
      client: "mysql2",
      connection: {
        host: "localhost",
        database: "myapp_test",
        user: "root",
        password: "password",
      },
    },
    // ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ
    production: {
      client: "mysql2",
      connection: {
        host: "prod-db.example.com",
        database: "myapp_prod",
        user: "root",
        password: process.env.DB_PASSWORD,
      },
    },
  },
};
๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋Œ€์ƒ:
  • _master๋กœ ๋๋‚˜๋Š” ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค
  • _slave๊ฐ€ ์•„๋‹Œ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค

์ผ๊ด„ ์ ์šฉ

# ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ์šฉ
pnpm migrate run

# ์ถœ๋ ฅ:
# Development Master: โœ“ 2 migrations applied
# Testing: โœ“ 2 migrations applied
# Production: โœ“ 2 migrations applied

์‹ค์ „ ์›Œํฌํ”Œ๋กœ์šฐ

1. Entity ๋ณ€๊ฒฝ

user.entity.ts
const UserEntity = {
  properties: [
    // ๊ธฐ์กด ํ•„๋“œ...
    {
      name: "profile_image",
      type: "string",
      nullable: true,
    },
  ],
};

2. ์ƒํƒœ ํ™•์ธ

pnpm migrate status
Prepared migrations:
  โ€ข 20240116_110230_add_profile_image_to_users

3. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ์šฉ

pnpm migrate run
โœ“ All migrations applied successfully!

4. ์ฝ”๋“œ ์—…๋ฐ์ดํŠธ

user.model.ts
class UserModelClass extends BaseModel {
  async updateProfileImage(userId: number, imageUrl: string) {
    await this.getPuri("w")
      .update({ profile_image: imageUrl })
      .where("id", userId);
  }
}

๊ณ ๊ธ‰ ์‚ฌ์šฉ๋ฒ• (ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ)

CLI ๋ช…๋ น์–ด๋กœ ์ œ๊ณต๋˜์ง€ ์•Š๋Š” ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์€ Migrator ํด๋ž˜์Šค๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜์—ฌ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Migrator ํด๋ž˜์Šค ์‚ฌ์šฉ

src/scripts/migration-tools.ts
import { Migrator } from "sonamu";

const migrator = new Migrator();

// ๋กค๋ฐฑ (๋งˆ์ง€๋ง‰ ๋ฐฐ์น˜)
await migrator.runAction("rollback", ["development_master"]);

// ํŠน์ • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค๋งŒ ์„ ํƒ
await migrator.runAction("apply", ["test"]);

// ์ƒํƒœ ํ™•์ธ (ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ)
const status = await migrator.getStatus();
console.log(status);

๋กค๋ฐฑ ์Šคํฌ๋ฆฝํŠธ

src/scripts/rollback-migration.ts
import { Migrator, Sonamu } from "sonamu";

Sonamu.runScript(async () => {
  const migrator = new Migrator();
  
  console.log("Rolling back last migration batch...");
  await migrator.runAction("rollback", ["development_master", "test"]);
  console.log("Rollback completed!");
});
์‹คํ–‰:
pnpm tsx src/scripts/rollback-migration.ts
๋ฐ์ดํ„ฐ ์†์‹ค ์œ„ํ—˜: ๋กค๋ฐฑ์€ ํ…Œ์ด๋ธ”์ด๋‚˜ ์ปฌ๋Ÿผ์„ ์‚ญ์ œํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋งค์šฐ ์‹ ์ค‘ํ•˜๊ฒŒ ์‚ฌ์šฉํ•˜์„ธ์š”.

์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์•ก์…˜

Migrator.runAction() ๋ฉ”์„œ๋“œ๊ฐ€ ์ง€์›ํ•˜๋Š” ์•ก์…˜:
์•ก์…˜์„ค๋ช…์‚ฌ์šฉ ์˜ˆ์‹œ
apply๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ์šฉ์ƒˆ ์Šคํ‚ค๋งˆ ๋ณ€๊ฒฝ ์ ์šฉ
rollback๋งˆ์ง€๋ง‰ ๋ฐฐ์น˜ ๋กค๋ฐฑ์‹ค์ˆ˜ํ•œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋˜๋Œ๋ฆฌ๊ธฐ

๋ฌธ์ œ ํ•ด๊ฒฐ

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ถฉ๋Œ

๋ฌธ์ œ: ์—ฌ๋Ÿฌ ๊ฐœ๋ฐœ์ž๊ฐ€ ๋™์‹œ์— ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒ์„ฑ
Error: Migration conflict detected
ํ•ด๊ฒฐ:
# 1. ์ตœ์‹  ์ฝ”๋“œ ๊ฐ€์ ธ์˜ค๊ธฐ
git pull

# 2. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ƒํƒœ ํ™•์ธ
pnpm migrate status

# 3. ํ•„์š” ์‹œ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์žฌ์ƒ์„ฑ
pnpm migrate run

๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์—ฐ๊ฒฐ ์‹คํŒจ

๋ฌธ์ œ: DB ์—ฐ๊ฒฐ ์‹คํŒจ
Error: connect ECONNREFUSED 127.0.0.1:3306
ํ•ด๊ฒฐ:
# 1. MySQL ์‹คํ–‰ ํ™•์ธ
mysql -u root -p -e "SHOW DATABASES;"

# 2. ์—ฐ๊ฒฐ ์ •๋ณด ํ™•์ธ
cat sonamu.config.ts

# 3. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ƒ์„ฑ
mysql -u root -p -e "CREATE DATABASE myapp_dev;"

๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ˆœ์„œ ์˜ค๋ฅ˜

๋ฌธ์ œ: ์ž˜๋ชป๋œ ์ˆœ์„œ๋กœ ์ ์šฉ๋จ
Error: Foreign key constraint violation
ํ•ด๊ฒฐ: ์™ธ๋ž˜ํ‚ค ์ฐธ์กฐ ๊ด€๊ณ„๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ, ์ฐธ์กฐ๋˜๋Š” ํ…Œ์ด๋ธ”์ด ๋จผ์ € ์ƒ์„ฑ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜์ • ๋ฐฉ๋ฒ• 1: ํ”„๋กœ๊ทธ๋ž˜๋งคํ‹ฑ ๋กค๋ฐฑ ํ›„ ์žฌ์ ์šฉ
// ๋กค๋ฐฑ
const migrator = new Migrator();
await migrator.runAction("rollback", ["development_master"]);

// ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ๋ช… ์ˆ˜์ • (ํƒ€์ž„์Šคํƒฌํ”„ ์กฐ์ •)
// 20240116_110230_*.ts โ†’ 20240116_110231_*.ts

// ์žฌ์ ์šฉ
await migrator.runAction("apply", ["development_master"]);
์ˆ˜์ • ๋ฐฉ๋ฒ• 2: Entity ์ •์˜ ์ˆœ์„œ ์กฐ์ •
// users ํ…Œ์ด๋ธ”์„ ๋จผ์ € ์ƒ์„ฑํ•˜๋„๋ก Entity ํŒŒ์ผ ์ˆœ์„œ ์กฐ์ •
// ๊ทธ ํ›„ pnpm migrate run

๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค

1. ์ž์ฃผ ์ ์šฉํ•˜๊ธฐ

# ์ž‘์€ ๋‹จ์œ„๋กœ ์ž์ฃผ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
pnpm migrate run  # ๋งค์ผ

2. ํ”„๋กœ๋•์…˜ ์ „ ํ…Œ์ŠคํŠธ

# ํ…Œ์ŠคํŠธ DB์—์„œ ๋จผ์ € ํ™•์ธ
pnpm migrate status
pnpm migrate run

# ๋ฌธ์ œ์—†์œผ๋ฉด ํ”„๋กœ๋•์…˜ ์ ์šฉ

3. ๋ฐฑ์—… ํ•„์ˆ˜

# ํ”„๋กœ๋•์…˜ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „ ๋ฐฑ์—…
mysqldump -u root -p myapp_prod > backup_$(date +%Y%m%d).sql

# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ ์šฉ
pnpm migrate run

4. ๋กค๋ฐฑ ๊ฐ€๋Šฅ์„ฑ ๊ณ ๋ ค

// โœ… ๋กค๋ฐฑ ๊ฐ€๋Šฅ - down() ํ•จ์ˆ˜๊ฐ€ ์ œ๋Œ€๋กœ ์ •์˜๋จ
export async function up(knex: Knex) {
  await knex.schema.alterTable("users", (table) => {
    table.string("phone").nullable();
  });
}

export async function down(knex: Knex) {
  await knex.schema.alterTable("users", (table) => {
    table.dropColumn("phone");
  });
}

5. Git์œผ๋กœ ๋ฒ„์ „ ๊ด€๋ฆฌ

# ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ํŒŒ์ผ์„ Git์— ์ปค๋ฐ‹
git add src/migrations/
git commit -m "Add phone field to users table"

# ํŒ€์›๋“ค๊ณผ ๊ณต์œ 
git push

๋‹ค์Œ ๋‹จ๊ณ„