[TypeORM] 쿼리빌더 (createQueryBuilder) - 외래키로 연결된 테이블 참조하는 법 / 부제: 좋아요 개수 가져오는 sql 구문

julia·2022년 3월 1일
1

Spoon Feed

목록 보기
3/5
post-thumbnail

API 개발을 시작하며 처음부터 난관에 봉착했었다. 다른게 아니라 SQL에 막힐 줄은 상상도 못했는데.. 정말 데이터베이스 공부의 필요성이 처음으로 피부로 와닿았다 🥲

데이터베이스 ERD

문제가 되는 테이블은 좋아요 모델이었다. 현재 유저와 정책을 좋아요 중개모델로 연결해주고 있는데(복합기본키 사용), count 컬럼을 따로 두지 않아 생 sql로 짜야했던 것.
장고에서는 orm이 다 편하게 뚝딱뚝딱 해준 덕에 좋아요 개수 셀 때 막혀본 적이 없는데 막상 raw sql을 생각하려고 하니 뇌가 멈췄다.
장고에 익숙했던 나는 처음에 n+1 문제가 있지 않을까 걱정도 들었고, 개수 컬럼을 따로 만드는게 더 수월할 것 같아서 컬럼 추가를 고민했지만.. 팀원 분과 상의해본 결과 n+1 은 raw와 가까운 쿼리빌더를 사용하면 피할 수 있고 현재 테이블 구조에서도 알 수 있는 likeCount를 따로 컬럼으로 두면 중복된 데이터가 될 것 같아 모델 변경은 하지 않기로 했다. 결과적으로는 sql 구문을 공부할 수 있게 되어 좋았다!!

현재 구조에서 좋아요 개수를 가져오는 sql 구문

아래와 같은 sql 구문을 입력하면 된다.

select id, name, like_count from dnd.policy left join (select policy_id, Count(policy_id) as like_count from dnd.like where like_check = true group by policy_id) as L on policy.id = L.policy_id;

어떤 일이 일어난 것인지 정리를 해보려고 한다.

이번 포스트의 주인공은

🧚 JOIN 🧚

이다.

Join의 개념

join 은 두 개 이상의 테이블을 연결해 데이터를 검색하는 방법이다.
종류는 inner join(교집합), left/right join(부분집합), outer join(합집합) 이 있고 outer의 경우 oracle 에서는 지원해주지만 mysql 사용 시 left + right 를 같이 사용해야 full outer join 을 구현할 수 있다.

기술 면접에서 inner joinouter join 의 차이를 물어보는 경우가 많다고 들어 추가적으로 기록해보자면 -- inner join 은 교집합이기 때문에 일치하지 않는 데이터를 모두 버리지만, outer join 에서는 일치하지 않는 레코드 또한 버리지 않고 NULL로 채워 결과를 반환한다.

작성한 구문 분석

필자가 사용한 left join 은 left 를 포함한 교집합이다. 구문은 위의 그림에서 확인할 수 있는데, 필자는 B에 해당하는 테이블에 서브쿼리를 적용했다.
서브쿼리는 반드시 괄호 안에 있어야 하며 예를 들어
(select policy_id, Count(policy_id) as like_count from dnd.like where like_check = true group by policy_id) as L
이라고 작성했을 시
like 테이블에서 like_check 컬럼이 true 인 레코드들의 policy_id, like_count (policy_id 의 개수를 센 결과) 를 선택해 policy_id 기준으로 그룹핑하고 이를 L 이라는 이름으로 부른다는 뜻이다.

참고: GROUP BY는 주로 aggregate 함수들(COUNT, MAX, MIN, SUM, AVG)과 함께 쓰인다.

결론적으로 A (policy) 테이블의 id와 B (as 로 이름 붙여준 새로운 테이블 L) 테이블의 policy_id 를 비교해 결과를 반환하게 된다.

쿼리빌더로 옮기기

sql 구문을 쿼리빌더로 옮기는 일도 만만치 않았다. typeorm의 createQueryBuilder는 raw sql과 비슷한 성능을 내는데 장고의 orm 보다는 실제 sql 구문과 비슷하고, 그렇다고 생 sql 구문과도 차이가 있기 때문에 처음 경험하는 나로서는 당황했기 때문이다.

아래 코드는 실제 우리 서비스의 코드이다. 카테고리(전체/주거/금융)를 입력받아 해당하는 데이터들을 보여주는 repository라고 보면 된다.

const findPolicyByCategory: (category: string) => Promise<Policy[]> = async (category) => {
    const result = await Policy.createQueryBuilder('policy')
        .select([
            'id',
            'name',
            'category',
            'summary',
            'application_period as applicationPeriod',
            'likeCount',
        ])
        .leftJoin(
            (qb) =>
                qb
                    .from(Like, 'like')
                    .select('COUNT(like.policy_id)', 'likeCount')
                    .where('like.like_check = true')
                    .addSelect('like.policy_id', 'policy_id')
                    .groupBy('like.policy_id'),
            'L',
            'policy.id = L.policy_id'
        )
        .addSelect('IFNULL(likeCount, 0)', 'likeCount') 부분
        .where('policy.category = :category', { category: category })
        .getRawMany();
    return result;
};

여기서 likeCount 를 가져왔어야 했는데 서브쿼리를 작성하는 게 생소해서 어려움을 겪었던 것 같다. 서브쿼리를 작성할 곳 안에 (qb) => qb.from ~ 이렇게 적어주면 된다. 나머지는 생 sql 과 거의 비슷하다.
콤마 옆에 새로운 이름을 붙여주면 거의 as 로 인식하게 되고, leftJoin 같은 경우 마지막 덩어리를 on 절로 인식한다. 추가적으로 leftJoin의 결과에서 데이터가 없는 경우 null로 가져오기 때문에 null을 0으로 바꿔주는 작업도 했다: addSelect('IFNULL(likeCount, 0)', 'likeCount') 부분.

그리고 getMany만을 사용하면 count 가 불러와지지 않는 이슈도 있었다. 여기 를 보면 getMany 와 getRawMany의 차이를 알 수 있는데

중요 부분 -- There are two types of results you can get using select query builder: entities or 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. But sometimes you need to select some specific data, let's say the sum of all user photos. This data is not an entity, it's called raw data. To get raw data, you use getRawOne and getRawMany.

필자처럼 count 를 한 데이터를 불러와야할 경우 이는 entity가 아닌 raw data 이기 때문에 getRawMany를 사용해야 함을 알 수 있다.

profile
Move Forward

0개의 댓글

관련 채용 정보