관리 메뉴

공부 기록장 💻

[NestJS/TypeORM] QueryBuilder for NestJS (TypeORM Refactoring) 본문

# Tech Studies/NestJS

[NestJS/TypeORM] QueryBuilder for NestJS (TypeORM Refactoring)

dream_for 2022. 10. 27. 00:41

- 공식문서 ( https://typeorm.io/select-query-builder) 를 정리하며, Query Builder을 이해해보자.

QueryBuilder란?

- TypeORM의 강력한 기능들 중 하나인데, 명쾌하고 편리한 구문을 이용해 SQL 쿼리들문을 작성하여, 자동적으로 실행하고 변형된 엔티티들을 자동적으로 얻을 수 있도록 하는 기능이다.

예를 들어보자면,
SQL문으로 user의 id가 1번인 user의 id, firstName, lastName을 출력하는 SQL문은 다음과 같다.

SELECT
    user.id as userId,
    user.firstName as userFirstName,
    user.lastName as userLastName
FROM users user
WHERE user.id = 1


DBMS에서 직접 SQL문을 작성하지 않고, TypeORM을 이용해 DB에 직접 접근하여 원하는 데이터를 조회하려면
다음과 같이 작성할 수 있다.

const firstUser = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .getOne()


QueryBuilder를 사용하는 이유?

(https://yceffort.kr/2021/07/dont-use-nodjs-orm)를 참고.

우선 ORM의 장점을 짚고 넘어가보자

- ORM (Object-relational mapping) 이 도입된 것은, 개발 프레임워크 내에서 생성하는 객체(Object, Model)와 데이터베이스의 관계(Table, Relation)를 연결(매핑)을 쉽게 하고, 애플리케이션의 객체를 DB로 쉽게 migration하는 작업을 편리하게 하기 위함이다.
- ORM의 장점으로는 중복 코드 방지, 다른 DB로 쉽게 교체/마이그레이션 가능, 여러 테이블에 쉽게 쿼리 날릴 수 있으며, 인터페이스를 작성하는 시간을 아껴 비즈니스 로직에 집중할 수 있다는 점이 있다.

그러나, ORM에 의존하지 말자

- DB 관계가 복잡해질 수록 기존 repository를 이용한 메서드들을 통한 CRUD 사용이 어려워지는 경우가 존재한다고 한다.
- 예를들어 1:N 관계의 user, board 릴레이션에 comment 댓글 기능을 추가한다고 가정하자. user와 board와 N:1 관계를 맺는 comment entity가 추가되었을 때, 관련된 userRepo.save(), boardRepo.save()가 오작동하게 되며, query builder로 refactoring을 해야 하는 경우가 생긴다.
- 따라서 관계를 맺은 특정 테이블을 조회하기 위해선 직접 query builder로 쿼리를 작성해야 한다.

- 결론은, 일부 쿼리는 ORM 작업으로 표현할 수 없다. 이러한 복잡한 쿼리는 직접 SQL문으로 작성하는 작업으로 회귀해야 한다. 예를 들어 다음과 같이, 쿼리 내에 서브 쿼리가 포함된 경우 혹은 table 간 join이 필요한 경우, ORM으로 명확하게 표현할 수 없다.

select * from users
where id not in (select user_id from boards where board_id=2)
and id in (select user_id from boards where board_id=3);


- 프레임워크 별로, 또 개발 시스템별로 다양한 ORM이 존재한다. 종류와 가짓수가 정말 많다. 모든 ORM을 이해하고 사용하는 것은 쉬운 일이 아니다..
- 따라서 결론은 SQL을 명확히 이해하고 DB를 다루는 기본적인 능력을 기르도록 하자.
- 쿼리를 손수 작성하는 방법도 있지만, SQL 쿼리에 대한 이해를 바탕으로, QueryBuilder을 사용하도록 하자.

QueryBuilder을 사용시 주의점

- QueryBuilder을 사용할 때, WHERE절을 표현에서 유일한 parameter을 제공해야 한다.
- 다음은 user 테이블과 pk로 연결된 linkedSheep, linkedCow 테이블을 조인하는 query builder 사용 예이다.
- 서로 다른 파라미터에 대해, id를 두번 사용하는 일이 없도록 하자. unique하게 지정된 sheepId, cowId로 조인하도록!

// id로 linkedSheep과 linkedCow를 찾으려는 경우 (X)
const result = await dataSource
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.linkedSheep', 'linkedSheep')
    .leftJoinAndSelect('user.linkedCow', 'linkedCow')
    .where('user.linkedSheep = :id', { id: sheepId })
    .andWhere('user.linkedCow = :id', { id: cowId });
    
// sheepId, cowId로, unique로 지정된 이름을 사용 (O)    
const result = await dataSource
    .createQueryBuilder('user')
    .leftJoinAndSelect('user.linkedSheep', 'linkedSheep')
    .leftJoinAndSelect('user.linkedCow', 'linkedCow')
    .where('user.linkedSheep = :sheepId', { sheepId })
    .andWhere('user.linkedCow = :cowId', { cowId });


QueryBuilder 생성하고 사용하기

- QueryBuilder을 이용해 개발 환경 내에서 쿼리를 생성하기 위해서는 다음과 같이 3가지의 방법이 존재한다.

1. DataSource 사용

- 직접 "user" 테이블을 select() 를 이용해 명시해줘야 한다.

const user = await dataSource
    .createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne()

2. entity manager 사용

- select() 대신, createQueryBuilder() 파라미터에 Entity 이름과, 테이블 이름을 지정한다.

const user = await dataSource.manager
    .createQueryBuilder(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne()

3. repository 사용

- getRepository()를 이용해 Entity를 지정한 후, createQueryBuilder()에는 테이블 이름을 지정한다.

const user = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id", { id: 1 })
    .getOne()


Select, Insert, Update, Delete, Relation QueryBuilder 사용

- QueryBuilder에는 총 5가지의 종류가 있다. 일반적으로 DB에서 사용하는 데이터 조작어(DML, Data Manipulation Language) 에 해당하는 select문, insert문, update문, delete문 그리고 마지막으로 새로운 관계(릴레이션)을 정의하기 위핸 relation 설정문이 존재한다.

1. SelectQueryBuilder

const user = await dataSource
    .createQueryBuilder()
    .select("user")
    .from(User, "user")
    .where("user.id = :id", { id: 1 })
    .getOne()

2. InsertQueryBuilder

- createQueryBuilder() -> insert() -> into() -> values() -> execute()

await dataSource
    .createQueryBuilder()
    .insert()
    .into(User)
    .values([
        { firstName: "Timber", lastName: "Saw" },
        { firstName: "Phantom", lastName: "Lancer" },
    ])
    .execute()

3. UpdateQueryBuilder

- createQueryBuilder() -> update() -> set() -> where() -> execute()

await dataSource
    .createQueryBuilder()
    .update(User)
    .set({ firstName: "Timber", lastName: "Saw" })
    .where("id = :id", { id: 1 })
    .execute()

4. DeleteQueryBuilder

- createqueryBuilder() -> delete() -> from() -> where() -> execute()

await dataSource
    .createQueryBuilder()
    .delete()
    .from(User)
    .where("id = :id", { id: 1 })
    .execute()

5. RelationQueryBuilder

- createQueryBuilder() -> relation(Entity, "연결할 relation") -> of(Entity의 pk) -> loadMany() ( 1:N 관계 설정)

await dataSource
    .createQueryBuilder()
    .relation(User,"photos")
    .of(id)
    .loadMany();


QueryBuilder로 데이터 값 가져오기

- getOne() : PK인 id값 혹은 unique로 지정한 컬럼의 값을 이용해 데이터를 가져오는 경우 사용

const timber = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id OR user.name = :name", { id: 1, name: "Timber" })
    .getOne()

- getOneOrFail() : 결과값이 존재하지 않는 경우 EntityNotFoundError 오류를 던지게 된다.

const timber = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .where("user.id = :id OR user.name = :name", { id: 1, name: "Timber" })
    .getOneOrFail()

- getMany() : 다수의 결과값을 가져오는 경우 사용 (예시- 전체 객체 가져오는 경우)

const users = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .getMany()

Raw Data 가져오기

- query builder을 통해 결과값을 도출할 때에는 entities 또는 raw results를 결과로 얻게 된다. 대부분의 경우, db로부터 실제 entity를 결과로 얻는 경우이다.
- 그러나 각 유저들의 사진 데이터의 합과 같이 구체적인 데이터 값을 도출해야 하는 경우에는 raw data를 결과값으로 얻어야 한다.

- getRawOne() : user id가 1번인 user의 전체 photosCount 합계 값 구하기

const { sum } = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .select("SUM(user.photosCount)", "sum")
    .where("user.id = :id", { id: 1 })
    .getRawOne()

- getRawMany() : user별로 photosCount 합계 값 구하기

const photosSums = await dataSource
    .getRepository(User)
    .createQueryBuilder("user")
    .select("user.id")
    .addSelect("SUM(user.photosCount)", "sum")
    .groupBy("user.id")
    .getRawMany()
    
    // [{ id: 1, sum: 25 }, { id: 2, sum: 13 }, ...] 와 같이 결과값 도출




[참고자료]
https://typeorm.io/select-query-builder
https://velog.io/@fj2008/NestjsTypeORM-QueryBuilder%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%95%BC-%ED%95%98%EB%8A%94-%EC%9D%B4%EC%9C%A0

728x90
반응형
Comments