🗄️ TypeORM4. QueryBuilder📖 개요

04-query-builder — QueryBuilder

이 챕터가 답하는 질문: find로 풀 수 없는 join·subquery·raw·having·union의 자리에서, QueryBuilder는 어떻게 SQL의 손잡이를 다시 손에 쥐여 주는가? 한 줄 답 (Pyramid Top): “QueryBuilder는 TypeORM의 추상의 탈출구다 — find가 표현할 수 없는 SQL을 타입 안전한 체이닝으로 다시 작성하게 해주는, ORM 위에 얹은 얇은 SQL DSL.”


한 문장 답 (Pyramid Top)

find/findOne단순 selection + 1-depth relations까지가 한계다. 복잡한 join 조건, 서브쿼리, HAVING, UNION, raw 표현은 모두 QueryBuilder의 자리다. QueryBuilder는 체이닝 메서드로 SQL 문장을 한 토막씩 조립하면서도 — :param 바인딩을 강제해 SQL injection을 차단하고, 결과를 엔티티로 매핑하는 단계를 선택적으로 끼워준다. 하지만 문자열 인터폴레이션 한 번이면 그 모든 안전장치가 무너지고, raw 결과 매핑을 잘못 끼우면 ORM의 의미가 절반쯤 사라진다. 이 챕터는 QueryBuilder의 언제 등장하는가부터 select·where·join·subquery·raw·pagination까지 6층으로 분해한다.


챕터 지도 (Mermaid)


Why — 왜 이 챕터를 별도로 빼는가

TypeORM을 얼마나 잘 쓰는가는 거의 전부 QueryBuilder를 얼마나 잘 다루는가로 결정된다. 세 가지 잘못된 직관이 거의 모든 사고의 출처다.

잘못된 직관실제어디서 다루나
”find 옵션을 깊게 키우면 다 풀린다”find의 join은 항상 LEFT JOIN이고, 조건부 join·subquery 비교는 불가능.01
”QueryBuilder도 어차피 SQL이니 문자열 그냥 끼우자”${userInput}을 SQL에 끼우는 모든 자리는 SQL injection 직행 통로다.02
skip/takeoffset/limit의 별명이다”join이 있을 때 skip/take별도 SELECT id 쿼리를 발사한다 — 결과가 다르다.06

이 챕터는 QueryBuilder를 언제 꺼내는가 → 무엇으로 조립하는가 → 어디서 추상을 깨는가 → 어떻게 자르는가의 네 층으로 본다.


How — 어떻게 읽나

다음 6개 문서를 순서대로 읽으면 약 70분이 걸린다. 각 문서는 독립적으로 읽혀도 되지만, 누적적이다.

#파일읽는 데핵심 키워드
0101-when-querybuilder.mdx10분find의 한계 · 복잡 join · having · union · raw 표현
0202-select-and-where.mdx12분select · addSelect · where · andWhere · :param · injection
0303-join-strategies.mdx14분innerJoin · leftJoinAndSelect · leftJoinAndMapOne/Many · raw vs entity
0404-subqueries.mdx12분where(qb => qb.subQuery()) · from(subquery) · EXISTS · IN (SELECT …)
0505-raw-and-getRawMany.mdx10분getRawMany vs getMany · raw 결과 매핑 · 추상화 깨는 시점
0606-pagination-and-counting.mdx12분skip/take · offset/limit · getManyAndCount · N+1 위험

의존성: 02는 01을, 03은 02를, 0405는 03을, 06은 0305를 가정한다.


What — 한 페이지 요약 (모든 문서의 핵심 한 줄)

문서한 줄 결론
01find읽기 쉬운 selection까지의 도구다 — 그 너머의 조건부 join·subquery·having·union은 QueryBuilder의 자리다.
02where('a = :x', { x })parameterized고, 문자열 인터폴레이션(예: `a = '${x}'`)은 SQL injection이다 — 둘은 시각적으로만 비슷하다.
03leftJoinAndSelect엔티티로 매핑되지만, leftJoinAndMapOne/Many임의 alias 결과를 엔티티 필드로 수동 매핑한다.
04서브쿼리는 QB 안의 QB고, from(subquery)서브쿼리를 테이블처럼 쓸 때 진가가 드러난다.
05getMany엔티티 객체를, getRawMany컬럼 dictionary를 준다 — 둘을 섞으면 ORM 의미가 절반 사라진다.
06skip/takejoin이 있을 때 별도 id 쿼리를 발사해 정확성을 지키지만 2번 round-trip의 비용을 낸다.

What-if — 이 챕터를 건너뛰면

  • 01(when)을 모르면: 모든 곳을 find 풀려다가 옵션이 10단 중첩된 거대 객체를 만든다.
  • 02(select/where)를 모르면: 검색창 입력을 where(`name LIKE '%${q}%'`) 식으로 끼워 넣고 — 어느 새벽 DROP TABLE을 만난다.
  • 03(join)을 모르면: 모든 join을 leftJoinAndSelect로 박아 Cartesian explosion으로 메모리 OOM을 만난다.
  • 04(subquery)를 모르면: 서브쿼리 자리에 raw 문자열을 끼워 바인딩이 깨진 SQL을 디버깅한다.
  • 05(raw)를 모르면: getRawMany 결과를 엔티티처럼 다루다가 relation 접근에서 undefined를 만난다.
  • 06(pagination)을 모르면: join + skip/take 조합에서 count가 안 맞는다틀린 쿼리를 추가하다가 더 망친다.

Insight — 한 단락 이야기

“QueryBuilder는 ORM의 겸손이 만든 추상이다”

ORM은 모든 SQL을 객체로 흡수하겠다는 야망에서 출발한다. 하지만 SQL은 40년에 걸쳐 정착한 모든 표현을 가진 언어고, 객체 그래프 탐색만으로는 WINDOW, LATERAL, WITH RECURSIVE, 복잡한 HAVING을 표현할 수 없다. TypeORM의 설계자는 그 한계를 인정하고 QueryBuilder를 얇은 SQL DSL로 끼워 넣었다 — find로 풀리는 자리는 find로, 안 풀리는 자리는 QueryBuilder로, 그래도 안 되면 raw로. 이 3단 사다리가 TypeORM의 실용주의다. 추상의 묘수는 “모든 것을 흡수하는 것”이 아니라 “흡수가 안 되는 자리를 드러내고 거기 손잡이를 놓는 것” — QueryBuilder는 그 손잡이의 이름이다.


Mermaid 4색 규약


한 단락 요약

QueryBuilder는 SQL의 손잡이를 다시 손에 쥐는 TypeORM의 탈출구다 — find가 표현 못하는 자리(01)를 체이닝 메서드로 조립한다. 그 조립의 최소 안전 단위:param 바인딩이고(02), 가장 자주 쓰이는 토막은 join(03)과 subquery(04)다. 추상이 부족할 땐 getRawManyraw 결과까지 내려가지만(05), 그 경계는 의식적으로 그어야 한다. 마지막에 결과를 자르는 pagination/counting은 join 유무에 따라 완전히 다른 SQL이 나간다(06). 이 챕터를 끝내면 “이건 find로 될까?” 대신 “이 SQL은 어느 손잡이에 가장 잘 맞나?” 라는 질문을 던지게 된다. 다음 챕터(05-transaction-datasource)는 이렇게 만든 쿼리들을 한 트랜잭션으로 묶는 길을 다룬다.