본문 바로가기

프로젝트

[기록, typeorm] ROW NUMBER + 서브쿼리로 대표 댓글 가져오기

ORM이 지원하는 메서드들이 편리하긴 하지만, 쿼리가 조금만 복잡해지면 직접 SQL 문을 작성해야 하는 경우가 많다. 최근 진행되는 팀 프로젝트에서 댓글을 가져오기 위해 TYPEORM이 지원하는 간단한 메서드 수준에서는 처리할 수 없는 동작이 요구되었다. 요구사항은 다음과 같다.

감정 별 대표 댓글 가져오기: 특정 기간(from, to) 내 감정 별로 공감 수가 최대인 댓글 하나씩 가져온다.

요점은 1. 특정 기간 내, 2. 감정 별, 3. 공감 수 최대에 있다. 감정 별로 묶은 후 공감 수에 대한 순위를 따져야 하므로 ROW_NUMBER을 이용해야 하는데, TYPEORM 수준에서 지원되는 기능은 아니므로  querybuilder을 이용하여 SQL문에 가까운 쿼리를 작성할 필요가 있다.

위 요건을 SQL문으로 표현하면 다음과 같다.

select id,content,sympathy,antipathy,emotion
from ( 
    select *,
    ROW_NUMBER() OVER ( PARTITION BY emotion ORDER BY sympathy DESC ) AS ranking 
    FROM analysis_comment
    WHERE createdAt BETWEEN '시작' AND '끝'
) AS temp
where ranking = 1;

 서브쿼리에서는 BETWEEN을 통해 쿼리 대상을 줄인 후,  PARTITION BY를 통해 감정 별로 구분하고, ORDER BY를 통해 공감 수가 많은 순서대로 순위를 지정한다. 이후 바깥에 있는 쿼리에서는 서브쿼리에서 구한 ranking 값이 1, 즉 감정 별 가장 많은 공감 수를 가지고 있는 댓글들만 가져온다.

 ROW_NUMBER을 활용하는 간단한 SQL문이지만, TYPEORM에서는 기본 값으로 지원하지 않기 때문에 querybuilder을 이용하여 직접 작성해야 한다. 다행히도 querybuilder 수준에서 서브 쿼리를 지정할 수 있으므로, 공식 문서를 보면서 서브쿼리를 작성해봤다. 공식 문서는 아래 주소로...

https://orkhan.gitbook.io/typeorm/docs/select-query-builder#using-subqueries

 

Select using Query Builder - typeorm

There are two types of results you can get using select query builder: entities and raw results. Most of the time, you need to select real entities from your database, for example, users. For this purpose, you use getOne and getMany. However, sometimes you

orkhan.gitbook.io

 공식 문서에서는 getQuery와 getParameters 방식과 select / from / where 내부에서 직접 서브쿼리 객체를 다루는 방식을 제시하고 있다.

 처음에 코드 분리를 위해 전자의 방식을 사용하려고 했는데, 각 값에 대해 별칭을 붙이도록 요구하고 있어 사용하는데 너무 불편함을 느꼈다. 내가 작성할 코드는 그리 복잡하지 않으므로 서브 쿼리 객체를 사용하더라도 큰 문제는 없어 보여서 좀 더 "우아한" 방식으로 쿼리를 작성했다. 

    const qb = this.dataSource
      .createQueryBuilder()
      .select(['id', 'content', 'sympathy', 'antipathy', 'emotion'])
      .from((subQuery) => {
        subQuery
          .select('*')
          .addSelect(
            'ROW_NUMBER() OVER ( PARTITION BY emotion ORDER BY sympathy DESC )',
            'ranking',
          )
          .from(AnalysisComment, 'comments')
          .where('keyword_id = :kid', {
            kid: keyword_id,
          });
        if (from && to)
          subQuery.andWhere('createdAt BETWEEN :from AND :to', { from, to });
        return subQuery;
      }, 'temp')
      .where('ranking = 1');
    return await qb.getRawMany();

  from 메서드 내부에서 서브 쿼리 객체를 직접 처리한다. 서브 쿼리에서 구한 값들을 외부 쿼리에서 그대로 사용하므로 특정 레포지토리의 queryBuilder을 가져오는 대신 dataSource로부터 바로 querybuilder을 생성했다.

 동작 결과는 다음과 같다. 테스트는 Swagger UI를 이용했으며, 정상적으로 데이터를 가져오는 모습을 볼 수 있었다.

가져온 데이터 목록


 ORM이 DBMS와 서버 사이의 종속성을 줄이고, 기본적인 메서드를 제공하여 개발을 편리하게 만들어준다는 점은 정말 좋지만, 결국 쿼리가 조금만 복잡해지더라도 SQL을 사용해아 하는 순간이 온다는 것을 생각하면, SQL을 완전히 배제하여 추상화하는 것은 어렵지 않나 싶다. 그럼에도 마이그레이션이 발생할 때 코드 상의 "일부분"만 수정해도 된다는 점은 엄청난 장점이기 때문에, ORM은 계속 사용될 것 같다.