포시코딩

3월26일 - TypeORM의 whereInIds를 사용해 Sub Query 구현하기 본문

TIL

3월26일 - TypeORM의 whereInIds를 사용해 Sub Query 구현하기

포시 2023. 3. 26. 17:22
728x90

개요

  async getMeetups(page: number, keyword: string): Promise<Meetup[]> {
    const pageLimit = this.configService.get('MEETUPS_PAGE_LIMIT') || 9;
    return await this.createQueryBuilder('m')
      .select([
        'm.id',
        // ...생략
        'm.createdAt',
        'j',
      ])
      .leftJoin('m.joins', 'j')
      .leftJoin('m.user', 'u')
      .where('m.title LIKE :keyword OR m.content LIKE :keyword', {
        keyword: `%${keyword}%`,
      })
      .orderBy('m.id', 'DESC')
      .take(pageLimit)
      .skip((page - 1) * pageLimit)
      .getMany();
  }

기존에 모임 목록을 가져오는 위와 같은 코드가 있었을 때

나는 내가 참여중인 모임의 목록만을 가져와야 하는 상황이 생겼다.

 

쌩 raw query로 작성하기엔 모임 목록을 가져올 때

join되는 참여중인 유저 목록 `join`을 array로서 포함해야 하기 때문에

TypeORM을 최대한 사용해보기로 했다.

 

getMeetups 메서드는 위와 같은 결과를 가져오고 있다.

 

그래서 생각한 방법이

일단 유저가 참여중인 모임의 id 목록을 구해 가져오고

해당 id 목록으로 위 코드를 통해 내가 원하는 참여한 모임 목록을 가져오는 방법이었는데

 

모임의 id 목록을 구하고 해당 목록을 돌리며 각각의 meetup에 대해 조회하는게 말이 안된다 생각했다.

joins만 아니었음 sub query 써서 끝냈을텐데 하며..

 

그래서 typeorm에서 sub query를 사용할 수 있는지를 찾아봤다.

구글링으로 이리저리 검색해본 결과 답이 안나와 ChatGPT의 도움을 받다가

 

whereInIds 키워드에 대해 알게 되었다.

 

whereInIds

알아보기 앞서 whereInIds에 대해선 직접적으로 검색해봐도 정보가 거의 없다. 

ChatGPT 아니었음 어떻게 알아내서 썼을지.. 

내가 사용한 방법은 다음과 같다.

 

1. 참여중인 모임 목록 구하기

SELECT j.userId, j.meetupId, m.title 
FROM `join` j
LEFT JOIN meetup m ON j.meetupId = m.id
WHERE m.deletedAt IS NULL
AND j.userId = 15
ORDER BY j.meetupId DESC;

일단 raw query 상으로 참여중인 모임 목록을 가져올 쿼리를 설계해봤다.

 

원하는대로 잘 가져왔고 이걸 토대로 meetupId에 매칭되게 

모임 정보를 붙여 가져와야 하니

userId, title은 필요 없어지고 가져오는 과정에서 정렬하면 되니 ORDER BY도 제외하면 된다.

 

const meetupIds = await this.joinRepository.createQueryBuilder('j')
  .select('j.meetupId', 'meetupId')
  .innerJoin('j.meetup', 'm')
  .where('m.deletedAt IS NULL')
  .andWhere('j.userId = :userId', { userId })
  .getRawMany();

그렇게 TypeORM 을 사용해 만든 결과물이다.

처음에는 innerJoin 대신 leftJoin을 썼었는데

 

join 테이블이 기준이다 보니 meetup 테이블 상의

deletedAt이 null이 아닌 데이터 (softDelete 된 데이터들)을 제외하지 못해

직접 where 을 써서 제외시켜 주었다.

 

그럼에도 deletedAt이 null인 데이터가 같이 들어왔는데 

leftJoin의 특성상 왼쪽의 모든 테이블에 대하여 오른쪽 테이블을 붙이게 발생한 문제로

innerJoin을 써서 해결할 수 있었다.

 

그 결과 내가 바란대로 참여중인 meetupId만 가져왔다.

나머지는 간단하다.

 

2. whereInIds 를 통해 sub query 구현

    return await this.createQueryBuilder('m')
      .select([
        'm.id',
        'm.userId',
        'u.email',
        'u.username',
        'm.title',
        'm.content',
        'm.place',
        'm.schedule',
        'm.headcount',
        'm.createdAt',
        'j',
      ])
      .leftJoin('m.joins', 'j')
      .leftJoin('m.user', 'u')
      .whereInIds(meetupIds.map((id) => id.meetupId))
      .andWhere('m.title LIKE :keyword OR m.content LIKE :keyword', {
        keyword: `%${keyword}%`,
      })
      .orderBy('m.id', 'DESC')
      .take(pageLimit)
      .skip((page - 1) * pageLimit)
      .getMany();

기존 코드에 whereInIds를 통해 위에서 구한 참여중인 모임 목록을 map으로 돌려 해당 id들로 찾게끔 만들었다.

 

그 결과 전체 모임 목록을 가져오던 코드가

내가 참여중인 모임 목록을 가져올 수 있게 바뀌었고

동시에 페이지네이션과 키워드 검색도 그대로 같이 할 수 있는 코드로 완성되었다.

 

후기

솔직히 말해서 전체 모임 목록 중 내가 만든 모임 목록을 구할 때도 

백엔드에서 위와 같은 코드를 구현하는 방법도 몰랐고 방법을 찾기도 오래 걸릴 것 같아 

리액트 상에서 가져온 목록중에서 찾게끔 해버렸었는데

 

이렇게 각 잡고 방법을 찾아보니 생각보다 빨리 해결하게 되었고

결과는 만족의 수준을 넘어 깨달음을 얻을 정도였다.

 

원하느 결과가 나온 순간 느껴지는 전율..

진짜 이 느낌 때매 개발하지 싶다.

 

최근 며칠간 어려운 걸 모두 끝내고 단순 반복 작업과 프로젝트 마무리 짓는 과정이

너무 귀찮아 지는 나태함이 생겼었는데

오늘을 통해 다시 한 번 리프레시 하며 다시 달려갈 기운을 얻을 수 있었다.

 

이렇게 억지로라도 새로운 기능과 어려운 기능 구현에 도전하는 습관을 들여야 겠다는 생각을 하며

위에서 구현한 기능을 어떤 전략을 통해 

api 상에서 전체 모임 / 내가 만든 모임 / 내가 참여한 모임 을 구분지어 호출하게끔 할지

다시 고민하러 가야겠다. 

끝!!

 

추가사항

포스팅 이후 계속 테스트 하다가 

내가 참여한 모임 목록에서 검색 기능과 함께 키워드가 전달 됐을 때 

참여한 목록중에서 키워드가 포함된 데이터가 아닌

참여한 목록이 무시되고 키워드가 포함된 데이터를 가져오는 에러가 있었는데

 

알고보니 where 문 안에서 or 을 괄호() 없이 쓰고 있어서 발생한 문제였다.

 

추가로 내가 만든 모임은 데이터 상 참여긴 하지만 논리적으로 '만든'거지 '참여한 건' 아니기 때문에

목록에서 제외하기로 팀원들과 상의하에 결정되었다.

 

이런 모든 추가 과정들이 더해져 결론적으로 작성된 코드는 아래와 같다.

 

  async getMeetupsWithJoined(userId: number, page: number, keyword: string): Promise<Meetup[]> {
    const pageLimit = this.configService.get('MEETUPS_PAGE_LIMIT') || 18;

    const meetupIds = await this.joinRepository.createQueryBuilder('j')
      .select('j.meetupId', 'meetupId')
      .innerJoin('j.meetup', 'm')
      .where('m.deletedAt IS NULL')
      .andWhere('j.userId = :userId', { userId })
      .andWhere('m.userId != :userId', { userId })
      .getRawMany();

    const tempQuery = this.createQueryBuilder('m')
      .select([
        'm.id',
        'm.userId',
        'u.email',
        'u.username',
        'm.title',
        'm.content',
        'm.place',
        'm.schedule',
        'm.headcount',
        'm.createdAt',
        'j',
      ])
      .leftJoin('m.joins', 'j')
      .leftJoin('m.user', 'u')
      .whereInIds(meetupIds.map((id) => id.meetupId));

    if (keyword !== '') {
      tempQuery.andWhere('(m.title LIKE :keyword OR m.content LIKE :keyword)', {
        keyword: `%${keyword}%`,
      })
    }

    const meetups = await tempQuery
      .orderBy('m.id', 'DESC')
      .take(pageLimit)
      .skip((page - 1) * pageLimit)
      .getMany();

    return meetups;
  }

기본적으로 whereInIds를 통해 내가 참여한 목록을 만들고

keyword가 있을 경우

(controller에서 keyword에 대해 default 값으로 ''을 넣어주고 있기 때문에 전달되지 않으면 '' 이다.)

 

andWhere문을 추가하는데 이 과정에서 위에서 겪은 문제 때문에

첫 번째 파라미터 문자열 안을 괄호로 감싸준 것을 확인할 수 있다.

 

마지막으로 기타 정렬이나 페이지네이션 옵션까지 추가하여 데이터를 받은 후 반환하면 끝.

728x90