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 BaseModelClass {
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
๋ค์ ๋จ๊ณ
fixture ํ
์คํธ ๋ฐ์ดํฐ ๊ด๋ฆฌํ๊ธฐ
Entity Entity ์ ์ ์์ธํ ์์๋ณด๊ธฐ