문제
Typeorm + Postgresql 을 사용하던 중 enum 타입으로 적용해놨던 컬럼에서 enum 타입의 구성을 바꾸는 과정을 진행했다.
기존 데이터를 보존하며 migration이 되게 진행해야 했다.
그냥 문자열 컬럼이었다면 간단했겠지만 enum 타입이라 많이 까다로웠다.
typeorm의 migration:genrate로 생성된 코드로는 제약 조건 오류가 발생하므로 직접 쿼리를 만들어줘야 한다.
enum 변경의 before, after는 다음과 같다.
before
export enum ProductDealState {
requested = "requested",
seller_response = "seller_response",
complete = "complete",
complete_requested = "complete_requested",
breakdown = "breakdown",}after
export enum ProductDealState {
requested = "requested",
seller_response = "seller_response",
complete = "complete",
complete_requested = "complete_requested",
deal_broken = "deal_broken",}breakdown을 deal_broken으로 변경해주는 작업이다.
우선 typeorm migration:generate로 코드를 생성하면 이렇게 나온다.
import { MigrationInterface, QueryRunner } from "typeorm"
export class mig1656467386655 implements MigrationInterface {
name = "mig1656467386655"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TYPE "public"."product_deal_history_state_enum" RENAME TO "product_deal_history_state_enum_old"`
)
await queryRunner.query(
`CREATE TYPE "public"."product_deal_history_state_enum" AS ENUM('requested', 'seller_response', 'complete', 'complete_requested', 'deal_broken')`
)
await queryRunner.query(
`ALTER TABLE "product_deal_history" ALTER COLUMN "state" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "product_deal_history" ALTER COLUMN "state" TYPE "public"."product_deal_history_state_enum" USING "state"::"text"::"public"."product_deal_history_state_enum"`
)
await queryRunner.query(
`ALTER TABLE "product_deal_history" ALTER COLUMN "state" SET DEFAULT 'requested'`
)
await queryRunner.query(
`DROP TYPE "public"."product_deal_history_state_enum_old"`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`CREATE TYPE "public"."product_deal_history_state_enum_old" AS ENUM('requested', 'seller_response', 'complete', 'complete_requested', 'breakdown')`
)
await queryRunner.query(
`ALTER TABLE "product_deal_history" ALTER COLUMN "state" DROP DEFAULT`
)
await queryRunner.query(
`ALTER TABLE "product_deal_history" ALTER COLUMN "state" TYPE "public"."product_deal_history_state_enum_old" USING "state"::"text"::"public"."product_deal_history_state_enum_old"`
)
await queryRunner.query(
`ALTER TABLE "product_deal_history" ALTER COLUMN "state" SET DEFAULT 'requested'`
)
await queryRunner.query(
`DROP TYPE "public"."product_deal_history_state_enum"`
)
await queryRunner.query(
`ALTER TYPE "public"."product_deal_history_state_enum_old" RENAME TO "product_deal_history_state_enum"`
)
}
}이 코드의 경우 product_deal_history 테이블에 state컬럼이 breakdown값으로 되어있는 row가 없다면 에러없이 수행된다.
breakdown 값으로 되어있는 row가 있다면 에러가 발생한다.
error: invalid input value for enum product_deal_history_state_enum: "breakdown"그렇다면 어떻게 해야할까?
개발 DB라면 데이터를 다 지우고 새로 적용하면 될 일이지만 운영 DB는 그렇게 할 수 없다.
내가 생각한 방법은 2가지가 있다.
product_deal_history테이블을product_deal_history_old테이블로 변경한 뒤 새로 테이블을 만들어서 old 테이블을 참조 및 비교하여 적절한 값을 새로운 테이블에 넣어준다.- state 컬럼을 state_old 로 변경한 뒤 새로 컬럼을 만들어서 기존 컬럼의 값을 참조 및 비교하여 적절한 값을 새로운 컬럼에 업데이트 해준다.
2번으로 진행해봤다.
기존 타입과 기존 컬럼을 old로 두고 새로운 타입과 컬럼을 만들어서 update 문으로 데이터를 넣어 주었다.
그 부분의 쿼리는 이렇다.
update "public"."product_deal_history" dest set state = src.state_old from "public"."product_deal_history" src where dest.id = src.id and src.state_old != 'breakdown'breakdown이 아닌 값들은 고스란히 유지하면 되니까 이렇게 해줬는데 타입이 맞지 않다고 에러가 뜬다.
error: column "state" is of type product_deal_history_state_enum but expression is of type product_deal_history_state_enum_old겹치는 문자열이라도 old 타입의 컬럼 값 그대로 새로운 컬럼에 넣는 것은 안되는 것 같다.
그래서 old 타입에 deal_broken도 추가해서 값 복사를 한 다음에 새로운 타입으로 변경해보려고 했다.
(alter type 명령어에 add value는 있는데 drop value는 없어서 새로운 타입을 만들어줘야 함.)
ALTER TYPE "product_deal_history_state_enum_old" ADD VALUE 'deal_broken'이것도 에러가 발생했다.
unsafe use of new value "deal_broken" of enum type product_deal_history_state_enum_old알아보니 트랜잭션 중에 타입에 새로운 값을 추가할 순 있지만 동일한 트랜잭션 내에서 그 값을 쓸수는 없었다.
해결
old 타입과 old 컬럼을 두고 새로운 컬럼은 old 타입으로 일단 지정한다.
같은 값으로 옮겨야 할 값들을 update 해준다.
breakdown → deal_broken으로 바뀌어야 하는 값은 따로 조건을 주어서 update를 한 후 type을 새로운 타입으로 변경 시켜준다.
이 과정에서 not null 설정도 에러가 뜨는 부분이 있어서 처음에는 not null 설정을 하지 않고 값 복사가 끝난 뒤에 not null 설정을 해주었다.
대략적인 순서와 코드는 이렇게 되었다.
import { MigrationInterface, QueryRunner } from "typeorm"
export class mig1656414845072 implements MigrationInterface {
name = "mig1656414845072"
/**
* enum 타입은 migration 과정이 복잡함.\
* 기존 타입을 old로 바꿔주고 new 타입을 생성한다.\
* 기존 컬럼을 old로 바꿔주고 new 컬럼을 아직은 old 타입으로 생성한다.\
* new 칼럼으로 바꾸지 않을 부분만 똑같이 데이터를 넣어준다.\
* new 컬럼에 아까 하지 못한 not null 설정과 default 설정을 해준다.\
* new 컬럼에 old 컬럼 값과 비교하여 바뀔 부분만 새로운 값을 넣어준다.\
* old 컬럼을 제거한다.\
* old 타입을 제거한다.
*/
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TYPE "public"."product_deal_history_state_enum" RENAME TO "product_deal_history_state_enum_old"`
)
await queryRunner.query(
`CREATE TYPE "public"."product_deal_history_state_enum" AS ENUM('requested', 'seller_response', 'complete', 'complete_requested', 'deal_broken')`
)
// custom
await queryRunner.query(
`alter table "public"."product_deal_history" rename column "state" to "state_old"`
)
await queryRunner.query(
`alter table "public"."product_deal_history" add "state" "public"."product_deal_history_state_enum_old"`
)
await queryRunner.query(
`update "public"."product_deal_history" dest set state = src.state_old from "public"."product_deal_history" src where dest.id = src.id and src.state_old != 'breakdown'`
)
await queryRunner.query(
`alter table "product_deal_history" alter column "state" type "public"."product_deal_history_state_enum" USING "state"::"text"::"public"."product_deal_history_state_enum"`
)
await queryRunner.query(
`alter table "product_deal_history" alter column "state" set default 'requested'`
)
await queryRunner.query(
`update "product_deal_history" set state = 'requested' where state is null`
)
await queryRunner.query(
`alter table "product_deal_history" alter column "state" set not null`
)
await queryRunner.query(
`update "public"."product_deal_history" set state = 'deal_broken' where state_old = 'breakdown'`
)
await queryRunner.query(
`alter table "public"."product_deal_history" drop column "state_old"`
)
// /custom
await queryRunner.query(
`DROP TYPE "public"."product_deal_history_state_enum_old"`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TYPE "public"."product_deal_history_state_enum" RENAME TO "product_deal_history_state_enum_old"`
)
await queryRunner.query(
`CREATE TYPE "public"."product_deal_history_state_enum" AS ENUM('requested', 'seller_response', 'complete', 'complete_requested', 'breakdown')`
)
// custom
await queryRunner.query(
`alter table "public"."product_deal_history" rename column "state" to "state_old"`
)
await queryRunner.query(
`alter table "public"."product_deal_history" add "state" "public"."product_deal_history_state_enum_old"`
)
await queryRunner.query(
`update "public"."product_deal_history" dest set state = src.state_old from "public"."product_deal_history" src where dest.id = src.id and src.state_old != 'deal_broken'`
)
await queryRunner.query(
`alter table "product_deal_history" alter column "state" type "public"."product_deal_history_state_enum" USING "state"::"text"::"public"."product_deal_history_state_enum"`
)
await queryRunner.query(
`alter table "product_deal_history" alter column "state" set default 'requested'`
)
await queryRunner.query(
`update "product_deal_history" set state = 'requested' where state is null`
)
await queryRunner.query(
`alter table "product_deal_history" alter column "state" set not null`
)
await queryRunner.query(
`update "public"."product_deal_history" set state = 'breakdown' where state_old = 'deal_broken'`
)
await queryRunner.query(
`alter table "public"."product_deal_history" drop column "state_old"`
)
// /custom
await queryRunner.query(
`DROP TYPE "public"."product_deal_history_state_enum_old"`
)
}
}