pnpm migrate ๋ช
๋ น์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๋ณ๊ฒฝ์ ์์ ํ๊ฒ ๊ด๋ฆฌํฉ๋๋ค. Entity ์ ์๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์๋์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
์ ์์ฑํ๊ณ , ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ผ๊ด๋๊ฒ ์ ์ฉํ ์ ์์ต๋๋ค.
๊ธฐ๋ณธ ๊ฐ๋
๋ง์ด๊ทธ๋ ์ด์
์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง ๋ณ๊ฒฝ์ ๋ฒ์ ๊ด๋ฆฌํ๋ ์์คํ
์
๋๋ค:
- Entity ๊ธฐ๋ฐ: Entity ์ ์์์ ์๋์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
์์ฑ
- ์์ฐจ ์คํ: ์์ฑ ์์๋๋ก ๋ง์ด๊ทธ๋ ์ด์
์ ์ฉ
- ๋ค์ค DB ์ง์: ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋์์ ๊ด๋ฆฌ
- ์๋ ๊ฐ์ง: Entity ๋ณ๊ฒฝ์ฌํญ ์๋ ๊ฐ์ง ๋ฐ ์ฝ๋ ์์ฑ
๋ช
๋ น์ด
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 - ๋ง์ด๊ทธ๋ ์ด์
์คํ
๋๊ธฐ ์ค์ธ ๋ง์ด๊ทธ๋ ์ด์
์ ๋ชจ๋ ์ ์ฉํฉ๋๋ค.
์คํ ๊ณผ์ :
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๊ฐ ์๋์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
์ ์์ฑํฉ๋๋ค.
// 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๋ ์ฌ๋ฌ ๋ฐ์ดํฐ๋ฒ ์ด์ค๋ฅผ ๋์์ ๊ด๋ฆฌํ ์ ์์ต๋๋ค.
๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์ฑ
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 ๋ณ๊ฒฝ
const UserEntity = {
properties: [
// ๊ธฐ์กด ํ๋...
{
name: "profile_image",
type: "string",
nullable: true,
},
],
};
2. ์ํ ํ์ธ
Prepared migrations:
โข 20240116_110230_add_profile_image_to_users
3. ๋ง์ด๊ทธ๋ ์ด์
์ ์ฉ
โ All migrations applied successfully!
4. ์ฝ๋ ์
๋ฐ์ดํธ
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
๋ค์ ๋จ๊ณ