Typeorm querybuilder에서 bigint number로 받기
typeorm에서 querybuilder를 사용해서 데이터를 뽑아오는 작업을 했다. postgres에서 pk를 Bigint로 설정 후 querybuilder에서 받아오니 무조건 string으로 가져오는 문제가 있었다. 공식 문서를 찾아 보니 bigint가 string으로 넘어오는 것은 의도한 동작으로 보인다.
https://typeorm.io/entities
하지만 타 ORM을 살펴보니 Bigint 타입은 바로 숫자로 사용이 되는 것으로 보였다. (JPA는 Long으로, Django는 정수 타입으로 바로 캐스팅) 그리고 기존의 개발된 코드를 보니 transformer를 사용해서 repository에서 바로 가져올 때는 number 타입으로 가져오는 것을 볼 수 있었다. 그래서 기존의 repository와 dao의 규칙을 유지하면서 코드의 가독성을 동시에 만족하기 위해서 decorator를 사용하기로 했다. (더 좋은 방법이 있을 수 있다. 그냥 decorator로 구현했을 뿐...)
제작
먼저 dao의 method에서 쿼리문이 실행되므로 메서드 데코레이터를 사용해야 할 것이다. 그리고 async한 작업이기 때문에 decorator에서도 promise한 데이터를 처리하기 위한 방법이 있어야 할 것이다.
또한 반환값에 대한 제한을 몇 개 걸어두어야 하는데 먼저 nested한 데이터는 없다고 생각해야 한다. 애초에 쿼리 결과값은 nested하게 출력이 되지 않기도 하고, nested한 데이터의 경우 가능한 경우의 수가 너무 많아지기 때문이다. 그리고 값은 항상 promise하게 온다고 생각해야 한다. db에 접근하는 async한 로직이기 때문에, 반환값은 항상 promise하다고 생각하자.
가능한 경우의 수를 정리하자. 먼저 nested하지 않은 단일 object의 경우에는 해당 value를 받아와서 number 타입을 Number()로 다시 시 씌워준다. 이 때 typeof로 number type을 분류하려고 했는데, interface에서 number로 지정해도 orm에서 string으로 반환해버리니 강지제로 string이 되어 버렸다. 그래서 decorator에 직접 property의 key를 받아서 Number로 씌워주기로 했다. 다음으로 array object의 경우에는 map 함수를 통해서 각 value마다 데이터 변환을 시도했다. 작업을 마친 결과물은 다음과 같다.
function ConvertToNumber(convertKey: string[]) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// 입력받은 value를 number로 변환
function convertValue(targetValue: any) {
Object.entries(targetValue).forEach(([key, value]) => {
if (convertKey.includes(key)) {
targetValue[key] = Number(value);
}
});
return targetValue;
}
const originalMethod = descriptor.value;
descriptor.value = async function (...args: any[]) {
const result = await originalMethod.apply(this, args);
// 배열일 경우
if (Array.isArray(result)) {
result.map((value) => convertValue(value));
return result;
}
// 단일 타입일 경우
convertValue(result);
return result;
};
return descriptor;
};
}
@CustomRepository(User)
export class TypeormUserDao extends Repository<User> implements UserDao {
@ConvertToNumber(['userId'])
async findAllByIds(ids: number[]): Promise<UserInfoResponse[]> {
if (ids.length === 0) {
return [];
}
return await this.createQueryBuilder('user')
.select('user.id', 'userId')
.addSelect('user.name', 'name')
.addSelect('user.nickname', 'nickName')
.addSelect('user.gender', 'gender')
.addSelect('user.grade', 'grade')
.addSelect('user.profileImageUrl', 'profileImageUrl')
.addSelect('organization.name', 'organizationName')
.innerJoin(Organization, 'organization', 'organization.id = user.organizationId')
.where('user.id IN (:...ids)', { ids })
.andWhere('user.status = :status', { status: AccountStatus.ACTIVATED })
.getRawMany<UserInfoResponse>();
}
}
해당 코드에서는 ConvertToNumber 데코레이터를 사용해서 값을 변환한다. 이 때 userId라는 key를 입력해 주어. UserInfoResponse.userId에 대해서 모두 컨버팅을 하게 하도록 했다.
나중에는 단일 object에 대한 변환, async 하지 않는 데이터에 대해서도 컨버팅을 할 수 있게 확장하면 좋을 것 같다.