NestJS에서 TypeORM의 Brackets를 사용해보자!

윤학·2023년 4월 23일
0

Nestjs

목록 보기
5/12
post-thumbnail

처음 모바일 앱 프로젝트를 진행했을 당시 아래와 같이 사용자가 선택한 정렬조건과 필터조건을 적용한 결과를 얻어야 하는 기능이 있었다.

각각의 조건들을 다 조합하여 조회했기 때문에 선택하지 않은 조건들은 조회를 하지 말았어야 했다.

단순히 전부 .andwhere()또는 .orwhere()로 연결하기에는 사용자가 요청하지 않은 조건을 체크하지 못했고

아래와 같이 쿼리문을 중간까지 작성하고 각 조건을 if문으로 검사해서 작성할 순 있었지만 어차피 Brackets을 사용해야 했기에 Brackets내부에서 작성을 하였다.

let 쿼리 = 데이터소스.createQueryBuilder().select().from()

if(사용자가 설정한 조건)
  	쿼리 = 쿼리.andWhere()
...

그럼 Brackets을 통해 해결했던 과정을 간단하게 살펴보자!

Brackets

TypeORM문서에 따르면 Brackets은 복잡한 where 조건문을 작성하는데 도움을 준다고 소개되어 있다.

복잡한 where문? 그냥 orWhere()이나 andWhere()를 계속 이어서 쓴다는 말인가?

일단 필터 로직의 일부를 보면서 동적 쿼리를 만들어보자.

.andWhere(
    new Brackets((qb) => {
        //일당 필터
        if (!!payFilter) {
            qb.andWhere('cg.pay <= :pay', {
                pay: payFilter === 'under10' ? 10 :
                    payFilter === 'under15' ? 15 : 20
            })
        }
        //시작가능일 필터
        if (!!startDateFilter) {
            qb.andWhere('cg.startDate <= :startDate', {
                startDate: startDateFilter === 'immediately' ? 1 : startDateFilter === '1week' ? 2
                    : startDateFilter === '2week' ? 3 : startDateFilter === '3week' ? 4 : 5
            })
        }
        //나이 필터
        if (!!ageFilter) {
            const { startAge, endAge } = getStartEndYear(ageFilter);
            qb.andWhere('user.birth between :startAge and :endAge', {
                startAge: endAge, endAge: startAge
            })
        }
        //성별 필터
        if (!!sexFilter) {
            qb.andWhere('user.sex = :sex', { sex: sexFilter === '남' ? '남' : '여' });
        }
        //지역 필터
        if (!!areaFilter) {
            const areaList = convertStringToLikeQuery(areaFilter);

            //첫번째 쿼리문 무조건 필터 한개는 들어오기
            switch (areaList.length) {
                case 1:
                    qb.andWhere('cg.possibleArea like :area', { area: areaList[0] });
                    break;
                case 2:
                    qb.andWhere(new Brackets((sub) => {
                        sub.where('cg.possibleArea like :firstArea ', { firstArea: areaList[0] })
                            .orWhere('cg.possibleArea like :secondArea', { secondArea: areaList[1] });
                    }))
                    break;
                case 3:
                    qb.andWhere(new Brackets((sub) => {
                        sub.where('cg.possibleArea like :firstArea ', { firstArea: areaList[0] })
                            .orWhere('cg.possibleArea like :secondArea', { secondArea: areaList[1] })
                            .orWhere('cg.possibleArea like :thirdArea', { thirdArea: areaList[2] });
                    }))
                    break;
            }
        }
    })
  )

생성한 Brackets 객체에서는 로직을 짤 수 있다.

그래서 if문으로 사용자가 선택하지 않은 필터는 내부 로직을 통해 조회를 하지 않을 수 있다.

undefined는 !!를 하면 false일 것이고 값이 들어있다면 true가 나올 것이기 때문에 클라이언트로부터 undefined값으로 넘어온 key는 조회를 건너뛸수 있다.

그럼 Brackets은 왜 쓰는것일까

생성된 Brackets 객체에서 추가한 where 조건들을 전부 조합하여 하나의 조건처럼 사용할 수 있기 때문이다.

위의 필터를 예로 들어보자.

남자면서 40대인 사람의 프로필들을 검색한다고 생각해보면 그냥 Brackets을 사용할 필요가 없이 andWhere()를 사용하면 된다.

하지만 남자면서 40대이고 인천이나 경기북부에 사는 사람의 프로필을 검색한다고 해보자.

SELECT `cg`.`id` as id, `cg`.`career` as career, `cg`.`pay` as pay, `cg`.`startDate` as startDate, `cg`.`possibleArea` as possibleArea, `cg`.`license` as license, `cg`.`keywords` as keywords, `cg`.`notice` as notice, `user`.`name` as name, `user`.`birth` as birth, `user`.`sex` as sex, `user`.`purpose` as purpose, `user`.`isCertified` as isCertified, `user`.`warning` as warning 
FROM `caregiver` `cg` 
INNER JOIN `user` `user` 
ON `user`.`id`=`cg`.`user_id` 
WHERE `user`.`profile_off` = ? AND `user`.`sex` = ? AND `user`.`birth` between ? and ? AND `cg`.`possibleArea` like ? OR `cg`.`possibleArea` like ? 

인천이나 경기북부를 선택한 부분에서 orWhere()로 연결이 될텐데 이럼 앞에 조건들에 상관없이 지역 하나만 일치해도 조회가 되어 버린다.

이러한 점을 Brackets을 통해서 해결할 수 있다.

위에서 Brackets에서 추가한 where 조건들을 하나의 조건으로 본다 했으므로 나이, 성별, 지역을 하나의 Brackets로 묶고 지역 내부의 세부 지역을 또 새로운 Brackets로 묶는 것이다.

WHERE `user`.`profile_off` = ? 
AND (`user`.`birth` between ? and ? AND `user`.`sex` = ? 
AND (`cg`.`possibleArea` like ?  OR `cg`.`possibleArea` like ? OR `cg`.`possibleArea` like ?))

풀어서 얘기한다면

'저는 사용자들 프로필 중 나이, 성별, 지역 3가지 조건이 제가 설정한 조건에 부합하는 프로필을 가져올거에요' -> 바깥 Brackets

'근데 지역 조건에서는 인천이나 제주도인 사람이면 돼요' -> 내부 Brackets

Brackets를 중첩으로 사용할 수가 있나?

사용할 수 있다.

Brackets 클래스의 생성자를 통해 WhereExpressionBuilder 인터페이스를 구현할 수 있는데

Brackets.d.ts

import { WhereExpressionBuilder } from "./WhereExpressionBuilder";
/**
 * Syntax sugar.
 * Allows to use brackets in WHERE expressions for better syntax.
 */
export declare class Brackets {
    readonly "@instanceof": symbol;
    /**
     * WHERE expression that will be taken into brackets.
     */
    whereFactory: (qb: WhereExpressionBuilder) => any;
    /**
     * Given WHERE query builder that will build a WHERE expression that will be taken into brackets.
     */
    constructor(whereFactory: (qb: WhereExpressionBuilder) => any);
}

WhereExpressionBuilder 인터페이스에서는 where 종류의 메소드들이 Brackets가 인수로 가능하다고 나와있다.

import { ObjectLiteral } from "../common/ObjectLiteral";
import { Brackets } from "./Brackets";
/**
 * Query Builders can implement this interface to support where expression
 */
export interface WhereExpressionBuilder {
    /**
     * Sets WHERE condition in the query builder.
     * If you had previously WHERE expression defined,
     * calling this function will override previously set WHERE conditions.
     * Additionally you can add parameters used in where expression.
     */
    where(where: Brackets, parameters?: ObjectLiteral): this;
    /**
     * Adds new AND WHERE condition in the query builder.
     * Additionally you can add parameters used in where expression.
     */
    andWhere(where: Brackets, parameters?: ObjectLiteral): this;
/**
     * Adds new OR WHERE condition in the query builder.
     * Additionally you can add parameters used in where expression.
     */
    orWhere(where: Brackets, parameters?: ObjectLiteral): this;
/**
 * @deprecated Use `WhereExpressionBuilder` instead
 */
export interface WhereExpression extends WhereExpressionBuilder {
}

주의할 점

문서에 Brackets 부분을 보면

If you use .where more than once you'll override all previous WHERE expressions.

이렇게 설명되어 있는데 처음에 이게 무슨 말인가 헷갈렸다.

.where을 한번 이상 사용하면 이전 where을 덮어쓴다는 얘기같아서 해봤더니

.where(조건1)
.where(조건2)
.andWhere()
...

이런식으로 코드를 짠다면 조건1이 조건2 때문에 무시된다.
밑에 andWhere 조건은 관계없이 수행이 된다.

그럼 Brackets에서도 동일할까?

.where(조건1)
.andWhere(new Brackets((qb) => {
	qb.where(조건2)
}))

이 경우에는 조건1은 무시되지 않는다.

하지만 동일한 Brackets 인스턴스에는 where을 중첩해서 사용한다면 똑같이 이전 where이 무시되는 것을 보면 인스턴스마다 하나의 where은 사용가능해 보인다.

마치면서...

Sql문을 잊어버리지 않으려고 QueryBuilder를 계속 사용했는데 이제는 더 편해진 것 같다.

그리고 TypeORM을 처음 사용할 때 Brackets에 대한 자료가 인터넷이나 문서에도 많이 없어서 시간을 허비했었는데

분명 알면 유용한 기능이라고 생각하지만 테이블을 더 잘 짜는 것이 중요한 느낌이다..

참고

Select using Query Builder
TypeORM find where conditions AND OR chaining

profile
해결한 문제는 그때 기록하자

0개의 댓글