본문으로 건너뛰기

TypeORM CreateDateColumn 데코레이터와 value transformer 문제

· 약 5분
Jake Son

문제 상황

보통 테이블에 기본으로 넣는 생성시간과, 수정시간 필드를 추가하기 위해 typeorm 사용하는 환경에서는 데코레이터와 상속을 사용한다.
예를들어 아래와 같은 클래스를 만든 후 다른 엔티티 클래스가 상속받는 방법으로 구현하게 된다.

import { CreateDateColumn, UpdateDateColumn } from "typeorm";

export abstract class BaseEntity {
@CreateDateColumn({ type: "timestamp" })
createdAt: Date;

@UpdateDateColumn({ type: "timestamp" })
updatedAt: Date;
}

하지만 각 필드의 타입을 Date 대신 서드파티 라이브러리의 날짜 타입으로 바꾸기 위해서는 value transformer 를 사용해야 하지만 에러가 발생한다.

import { CreateDateColumn, UpdateDateColumn } from "typeorm";
import { LocatDateTime } from "@js-joda/core";

export abstract class BaseEntity {
@CreateDateColumn({
type: "timestamp",
transformer: new LocalDateTransformer(),
})
createdAt: LocalDateTime;

@UpdateDateColumn({
type: "timestamp",
transformer: new LocalDateTransformer(),
})
updatedAt: LocalDateTime;
}
정보

여기서 LocalDateTransformer 는 typeorm 의 ValueTransformer 인터페이스를 구현한 클래스로 js 의 Date 와 LocalDateTime 간의 변환로직이 있다.

typeorm 공식 저장소에 이와같은 이슈가 올라와 있지만 (2020년 12월) 아직도 해결되지 않고있다.

해결 방안

이를 해결하기 위해 @CreateDateColumn 데코레이터 대신 @Column, @BeforeInsert, @BeforeUpdate 를 활용한다.

import { BeforeInsert, BeforeUpdate, Column } from "typeorm";
import { LocalDateTime } from "@js-joda/core";

export abstract class BaseEntity {
@Column({
type: "timestamptz",
transformer: new LocalDateTimeTransformer(),
nullable: false,
update: false,
})
createdAt: LocalDateTime;

@Column({
type: "timestamptz",
transformer: new LocalDateTimeTransformer(),
nullable: false,
})
updatedAt: LocalDateTime;

@BeforeInsert()
protected beforeInsert() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}

@BeforeUpdate()
protected beforeUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

@BeforeInsert@BeforeUpdate 는 이름 그대로 테이블에 새로운 row 가 추가되기 전이나 기존 row 수정작업 전에 실행된다.
정상 동작하는지 테스트코드를 작성해보자.

import {
Connection,
createConnection,
Entity,
PrimaryGeneratedColumn,
Repository,
} from "typeorm";
import { LocalDateTime } from "@js-joda/core";

@Entity()
class TestEntity extends BaseEntity {
@PrimaryGeneratedColumn("increment")
id: number;
}

describe("TestEntityRepository", () => {
let testEntityRepository: Repository<TestEntity>;
let connection: Connection;

beforeAll(async () => {
connection = await createConnection({
type: "postgres",
host: "localhost",
port: 5432,
username: "test",
password: "test",
database: "test",
entities: [TestEntity],
synchronize: true,
});

testEntityRepository = connection.getRepository(TestEntity);
});

afterAll(() => connection.close());

beforeEach(() => testEntityRepository.clear());

it("save 메소드로 insert 시 createdAt, updatedAt 이 현재시간으로 들어간다", async () => {
// given
const nowTime = LocalDateTime.now();
const entity = new TestEntity();

// when
const testEntity = await testEntityRepository.save(entity);

// then
expect(testEntity.createdAt.isAfter(nowTime)).toBeTruthy();
expect(testEntity.updatedAt.isAfter(nowTime)).toBeTruthy();
});

it("insert 메소드로 insert 시 createdAt, updatedAt 이 현재시간으로 들어간다", async () => {
// given
const nowTime = LocalDateTime.now();
const entity = new TestEntity();

// when
await testEntityRepository.insert(entity);

// then
const testEntity = await testEntityRepository.findOneOrFail();
expect(testEntity.createdAt.isAfter(nowTime)).toBeTruthy();
expect(testEntity.updatedAt.isAfter(nowTime)).toBeTruthy();
});

it("save 메소드로 엔티티 업데이트 시 updatedAt 이 갱신된다", async () => {
// given
const testEntity = await testEntityRepository.save(new TestEntity());
const before = testEntity.updatedAt;

// when
const updatedTestEntity = await testEntityRepository.save(testEntity);

// then
expect(updatedTestEntity.updatedAt.isAfter(before)).toBeTruthy();
});

it("update 메소드로 엔티티 업데이트 시 updatedAt 이 갱신된다", async () => {
const testEntity = await testEntityRepository.save(new TestEntity());
const before = testEntity.updatedAt;

// when
await testEntityRepository.update(testEntity.id, testEntity);

// then
const updatedTestEntity = await testEntityRepository.findOneOrFail();
expect(updatedTestEntity.updatedAt.isAfter(before)).toBeTruthy();
});
});

대부분의 상황에서 정상적으로 동작하지만 queryBuilder 를 사용해 update 를 수행할 때에는 updatedAt 이 갱신되지 않는다.

it("queryBuilder 로 업데이트 시 updatedAt 갱신되지 않는다", async () => {
// given
const testEntity = await testEntityRepository.save(new TestEntity());
const before = testEntity.updatedAt;

// when
await testEntityRepository
.createQueryBuilder("testEntity")
.update()
.set({ id: 3 })
.execute();

// then
const updatedTestEntity = await testEntityRepository.findOneOrFail();
expect(updatedTestEntity.updatedAt.isEqual(before)).toBeTruthy();
});

위 경우에는 명시적으로 현재시간을 넣어주어야 한다.

it("queryBuilder 로 업데이트 시 updatedAt 갱신된다", async () => {
// given
const testEntity = await testEntityRepository.save(new TestEntity());
const before = testEntity.updatedAt;

// when
await testEntityRepository
.createQueryBuilder("testEntity")
.update()
.set({ id: 100, updatedAt: LocalDateTime.now() })
.execute();

// then
const updatedTestEntity = await testEntityRepository.findOneOrFail();
expect(updatedTestEntity.updatedAt.isAfter(before)).toBeTruthy();
});