문제 상황
보통 테이블에 기본으로 넣는 생성시간과, 수정시간 필드를 추가하기 위해 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();
});