[NEST] TypeORM Take와 Limit의 차이

InnomDB·2022년 12월 26일
0

typeorm

목록 보기
5/5

비슷한 목적을 가진 다른 TypeORM 방법.

우리는 흔히 Pagination 또는 Id를 받아 무한 스크롤을 구현할 때 take() 또는 skip()을 사용하여
데이터의 개수 제한을 둡니다.

허나 어느 상황에 take()를 써야하고 skip()을 써야할지 정확히 아는것이 중요합니다.
이번 시간에는 이 두가지의 차이에 대해 알아보겠습니다.

먼저 limit에 대해 알아보겠습니다.
TypeORM에서 소개하는 Limit의 내용입니다.

     * Set's LIMIT - maximum number of rows to be selected.
     * NOTE that it may not work as you expect if you are using joins.
     * If you want to implement pagination, and you are having join in your query,
     * then use instead take method instead.

하나씩 하나씩 살펴보겠습니다.

  1. 선택할 최대 행 수를 지정합니다.
  2. 만약 조인을 사용한다면 예상하지 못한 동작이 일어날 수 있습니다.
  3. 만약 페이지네이션을 구현하고 싶고 쿼리에 조인을 사용해야한다면 LIMIT대신 take를 사용하세요.

TypeORM은 JOIN을 사용한다면 LIMIT을 지양하라고 강조하고 있습니다.
그 이유가 무엇일까요?

단순한 말 대신 코드와 쿼리로 보면서 설명하겠습니다.

우선 News와 NewsToThemes entity가 있다고 가정하겠습니다.

News

id: number;
title: string;
content: string;
createdAt: Date;
publishAt: Date;

NewsToThemes

id: number;
newsId: number;
theme: string;

가짜로 생각하고 만들다 보니 조금 엉성할 수 있어도 이해해주시길 바랍니다.

const builder = this.getRepository(News).createQueryBuilder('news')
.leftJoinAndSelect('news.newsToThemes').limit(10);

위의 결과 쿼리는 무엇일까요?

SELEECT * FROM news LEFT JOIN news_to_themes ON news.id = news_to_themes.news_id LIMIT 10

위와 같은 결과가 나올 것입니다. (*은 귀찮아서 모든 컬럼값을 대체했습니다.)

위의 쿼리는 LIMIT 10이 먼저 평가됩니다.

이제 take메소드를 확인해보겠습니다.

const builder = this.getRepository(News).createQueryBuilder('news')
.leftJoinAndSelect('news.newsToThemes').take(10);
EXPLAIN SELECT 
  DISTINCT `distinctAlias`.`news_id` AS `ids_news_id` 
FROM 
  (
    SELECT 
      `news`.`id` AS `news_id`, 
      `news`.`title` AS `news_title`, 
      `news`.`content` AS `news_content`, 
      `news`.`publish_at` AS `news_publish_at`, 
      `news`.`created_at` AS `news_created_at`, 
      `newsToThemes`.`id` AS `newsToThemes_id`, 
      `newsToThemes`.`news_id` AS `newsToThemes_news_id`, 
      `newsToThemes`.`theme` AS `newsToThemes_theme`, 
    FROM 
      `news` `news` 
      LEFT JOIN `news_to_themes` `newsToThemes` ON `newsToThemes`.`news_id` = `news`.`id` 
  ) `distinctAlias` 
LIMIT 10;

위와 같은 쿼리가 날라가게 됩니다.

둘의 차이가 느껴지시나요 휴먼?

take는 서브쿼리로 조인을 먼저 한 후 LIMIT을 적용시킵니다.
따라서 조인을 할 경우에는 take로 조인의 식을 먼저 평가 후 LIMIT으로 제한을 두어야 내가 원하는 결과가 제대로 나오게 되는 것입니다.

근데 take는 여기서 끝이 아닙니다!!!
한가지 쿼리가 더 날라가는데 그것은 바로 두두두두두둥탁!

    SELECT 
      `news`.`id` AS `news_id`, 
      `news`.`title` AS `news_title`, 
      `news`.`content` AS `news_content`, 
      `news`.`publish_at` AS `news_publish_at`, 
      `news`.`created_at` AS `news_created_at`, 
      `newsToThemes`.`id` AS `newsToThemes_id`, 
      `newsToThemes`.`news_id` AS `newsToThemes_news_id`, 
      `newsToThemes`.`theme` AS `newsToThemes_theme` 
    FROM 
      `news` `news` 
      LEFT JOIN `news_to_themes` `newsToThemes` ON `newsToThemes`.`news_id` = `news`.`id` 
WHERE 
  (
    `news`.`id` IN (
      1, 2, 3, 4, 5, 6, 
      7, 8, 9, 10
    )
  )

처음 날린 쿼리의 결과를 객체로 받아 그 id값으로 다시 한번 10개의 데이터를 얻어오는 것입니다!!
여기까지만 보고 아~~ 그렇구나 하고 끝낸다면 나중에 후회하게 될것입니다!!

핵심은 바로 결과를 객체로 받는다입니다!! 객체로 받으면 객체로 받는거지 뭐가 그렇게 중요한데?? 싶으실텐데 take 또는 limit을 혼자만 쓸 때는 문제가 없습니다! 허나 orderBy를 붙인다면 얘기가 달라지게 됩니다.

const builder = this.getRepository(News).createQueryBuilder('news')
.leftJoinAndSelect('news.newsToThemes').limit(10).orderBy('news.id', 'DESC');

위의 코드는 문제 없이 정상동작합니다.
허나 아래의 코드를 살펴보겠습니다!!

const builder = this.getRepository(News).createQueryBuilder('news')
.leftJoinAndSelect('news.newsToThemes').limit(10).orderBy('news.publish_at', 'DESC');

이것은 정상동작할까요?

이것은 에러입니다!! 삐삑 에러 발견!!

Cannot read properties of undefined (reading 'databaseName')

위와 같은 에러메세지가 자기주장을 하면서 나타날것입니다!!

그 이유가 바로 위에서 말씀드렸던 객체로 반환하기 때문입니다!!
이제 Nest로 우리가 Entity 정의하는 부분을 살펴보겠습니다.

@Entity()
export class News {
 @PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
 id: number;
  
  @Column({ type: 'datetime', width: 3 })
  publishAt: Date;
}

보통 위와 같이 사용합니다.
DB의 값 _(스네이크 케이스)가 TS로 넘어오면서 캐멀케이스가 되고 TS값이 DB로 넘어갈때 스네이크 케이스로 변환되어야 되기 때문에 namingStragry를 사용합니다. 그게 아니라면 아래와 같이 매번 정의해야 할 것입니다.

@Entity()
export class News {
 @PrimaryGeneratedColumn({ type: 'bigint', unsigned: true })
 id: number;
  
  @Column({ type: 'datetime', width: 3, name: 'publish_at' })
  publishAt: Date;
}

따라서 take와 Order By를 함께 사용할 때에는 객체로 변경된 값을 사용하셔야 됩니다.

아래 살짝 정리해드리겠습니다.

use `.orderBy('entity.creatdAt', 'DESC')` with `.skip()` and `.take()`

use `.orderBy('entity.creatd_at', 'DESC')` with `.offset()` and `.limit()`

limit 또는 offset은 원시 필드 이름, take, skip은 entity필드 이름을 사용하는 것이 정답입니다.
그게 알아서 값이 바뀌더라도 위의 내용을 기억하시고 사용하시면 나중에라도 도움이 될 것입니다.

위에서 언급한 예는 항상 getMany() 또는 getOne()을 사용할때로 가정합니다.
getRawMany()는 피하는것이 좋습니다.

스택오버플로우
티스토리 8:20 개발실Log
GitHub TypeORM
GitHub TypeORM 제일 도움된 문서

profile
이노오오옴

0개의 댓글