카테고리 없음

[TypeORM] TypeORM에서 @ComputedColumn 사용하기

29din 2024. 8. 12. 23:14

쿼리 결과를 코드에서 사용하기 위해 ORM을 통해 객체로 변경할 때, 미묘한 어긋남이 발생할 때가 있다.

예를 들어, 어떤 게시판의 게시글 중 유저별로 북마크했는지 안했는지 여부를 나타내야할 때, 이 값은 게시글 엔티티의 칼럼에 저장하기 보다는 그때 그때 계산하는 것이 좋을 것이다.


DB에 저장된 값이 아닌 계산된 값을 사용해야할 때 TypeORM에서는 @VirtualColum 데코레이터를 제공한다.

위 방법은 내가 구하고자 하는 값이 이미 DB 내부의 정보들로 결정되는 값일 경우에만 사용할 수 있다는 단점이 있다. 즉, 회사의 직원 수를 나타내는 칼럼은 코드 내부에서 값을 매번 업데이트하기보다 요청 시에 Count 쿼리를 통해 그때그때 확인할 수 있다면 해당 데이터에 대한 신뢰도가 올라갈 것이다.

 

앞에서 예를 들었던 '유저별로' 북마크 여부를 나타내기 위해서는 유저 아이디 등의 추가 정보를 요청 시에 전달해야한다.

 

1. 첫번째 구현

이를 위해 추가로 데코레이터 하나를 생성한다. 해당 방법은 검색을 통해 쉽게 확인할 수 있다.

// decorator.ts

import "reflect-metadata";
export const VIRTUAL_COLUMN_KEY = Symbol("VIRTUAL_COLUMN_KEY");
export function VirtualColumn(name?: string): PropertyDecorator {
  return (target, propertyKey) => {
    const metaInfo = Reflect.getMetadata(VIRTUAL_COLUMN_KEY, target) || {};
    metaInfo[propertyKey] = name ?? propertyKey;
    Reflect.defineMetadata(VIRTUAL_COLUMN_KEY, metaInfo, target);
  };
}

 

Reflect를 이용해서 메타데이터를 설정한다. 위 코드에서는 데코레이터의 인자로 name을 설정할 경우 나중에 쿼리에서 name으로 SELECT한 값을 할당할 수 있다.

 

예를 들어 property의 이름을 'isBookmarked'로 두고 @VirtualColumn('bookmark') 데코레이터를 붙일 경우,

SELECT ..., "bookmark" as ...
등의 쿼리로 선택한 bookmark의 값을 isBookmarked 속성에 넣을 수 있는 것.

 

데코레이터로 묶은 속성에 우리가 원하는 값을 넣을 수 있도록  이제 polyfill.ts를 작성한다.

// polyfill.ts

import { SelectQueryBuilder } from 'typeorm';
export const VIRTUAL_COLUMN_KEY = Symbol('VIRTUAL_COLUMN_KEY');

declare module 'typeorm' {
  interface SelectQueryBuilder<Entity> {
    getMany(this: SelectQueryBuilder<Entity>): Promise<Entity[] | undefined>;
    getOne(this: SelectQueryBuilder<Entity>): Promise<Entity | undefined>;
  }
}

SelectQueryBuilder.prototype.getMany = async function () {
  const { entities, raw } = await this.getRawAndEntities();

  const items = entities.map((entitiy, index) => {
    const metaInfo = Reflect.getMetadata(VIRTUAL_COLUMN_KEY, entitiy) ?? {};
    const item = raw[index];

    for (const [propertyKey, name] of Object.entries<string>(metaInfo)) {
      entitiy[propertyKey] = item[name];
    }

    return entitiy;
  });

  return [...items];
};

SelectQueryBuilder.prototype.getOne = async function () {
  const { entities, raw } = await this.getRawAndEntities();
  const metaInfo = Reflect.getMetadata(VIRTUAL_COLUMN_KEY, entities[0]) ?? {};

  for (const [propertyKey, name] of Object.entries<string>(metaInfo)) {
    entities[0][propertyKey] = raw[0][name];
  }

  return entities[0];
};

 

 

위 코드는 SelectQueryBuilder의 getOne과 getMany 메서드를 새롭게 정의한다. getRawAndEntites 메서드를 통해 쿼리 결과와 일치하는 엔티티를 모두 구할 수 있다. 

 

2. 두번째 구현

만약 매번 SELECT절을 추가하고싶지 않다면 Default 값을 설정할 수 있다. 여기선 KEY값을 'VIRTUAL_PROPERTY_KEY'로 변경하였다.

// decorator.ts

export const VIRTUAL_PROPERTY_KEY = Symbol('VIRTUAL_PROPERTY_KEY');

type VirtualPropertyOptions = {
  name?: string;
  default?: any;
};

export function VirtualProperty(
  options?: VirtualPropertyOptions,
): PropertyDecorator {
  return (target, propertyKey) => {
    const metaInfo = Reflect.getMetadata(VIRTUAL_PROPERTY_KEY, target) || {};

    metaInfo[propertyKey] = {
      name: options?.name ?? propertyKey,
      default: options?.default,
    };

    Reflect.defineMetadata(VIRTUAL_PROPERTY_KEY, metaInfo, target);
  };
}

 

메타데이터를 string이 아닌 Object로 설정한다. 

 

그리고 다음과 같이 변경한다.

type VirtualPropertyOptions = {
  name: string;
  default?: any;
};

...

for (const [propertyKey, options] of Object.entries<VirtualPropertyOptions>(metaInfo)) {
    entity[propertyKey] = item[options.name] ?? options.default;
}

 

3. 세번째 구현

위의 구현의 단점은 raw의 index와 entity의 index가 일치해야한다는 것이다. 만약 배열이 정규화되어 있다면 하나의 Entity에 할당될 Raw가 여러 개 존재할 수 있다. 내가 겪은 문제도 이와 같았는데, 북마크를 추가했는데 DB에는 제대로 반영되는데 프론트에서 확인할 때는 엉뚱한 항목이 북마크된 것으로 보였다...

 

PrimaryKey를 이용해서 같은 Raw를 판단한다.

PrimaryKey를 구하는 방법은 여러가지가 있겠지만 여기서는 다음과 같은 방법으로 결정한다.

  const primaryKey = 'id';
  const mergedRawData = mergeRawData(raw, `${this.alias}_${primaryKey}`);
const mergeRawData = (rawData: any[], primaryKey: string): any =>
  rawData.reduce(
    (acc, item) => ({
      ...acc,
      [item[primaryKey]]: acc[item[primaryKey]]
        ? { ...acc[item[primaryKey]], ...item }
        : { ...item },
    }),
    {},
  );
...
const item = mergedRawData[entity[primaryKey]] ?? {};
...

 

이렇게하면 Raw가 여러 개 나올 때 발생하는 문제를 해결할 수 있다.

출처 : 
https://blog.devgenius.io/virtual-column-and-computer-column-solutions-for-typeorm-in-nestjs-7a4d44b34923

 

Virtual Column and Computed Column solutions for TypeORM in Nestjs

If you fully appreciate the technique of Computed Properties on the front end, then you definitely would like to implement it on the…

blog.devgenius.io

https://orkhan.gitbook.io/typeorm/docs/decorator-reference#virtualcolumn

 

Decorator reference | typeorm

Marks your model as an entity. Entity is a class which is transformed into a database table. You can specify the table name in the entity: This code will create a database table named "users". View entity is a class that maps to a database view. expression

orkhan.gitbook.io