name vs referencedColumnName

엔티티 필드에 연관관계를 설정할 때 @JoinColumn 어노테이션을 사용할 수 있다.

@JoinColumn 의 속성 중 name 과 referencedColumnName 가 혼동돼서 정리해두려고 한다.

두 속성은 외래키와 연관이 있다. 외래키는 한 테이블에서 다른 테이블의 행을 식별하기 위해 사용한다.


name

The name of the foreign key column.


자신의 테이블에 선언한 외래키 컬럼명이다.

다른 테이블의 행을 식별하기 위해 해당 테이블에 선언한 외래키의 컬럼명을 뜻한다.


referencedColumnName

The name of the column referenced by this foreign key column.


참조하는 테이블에 선언된 컬럼명이다.

자신의 테이블에 선언한 외래키로 조회하는 참조 테이블의 컬럼명을 나타낸다.

별도로 선언하지 않으면 디폴트로 참조 테이블의 PK(Primary Key) 컬럼명이 설정된다.


예시

create table post 
(
    id int primary key auto_increment,
    title varchar(50) not null,
    content varchar(200) not null
);
create table comment 
(
    id int primary key auto_increment,
    post_id int not null,
    content varchar(200) not null,
    foreign key (post_id) references post(id)
);

post, comment 테이블의 구조는 위와 같다.



@Entity
@Table(name = "post")
public class Post {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 50, nullable = false)
    private String title;

    @Column(length = 200, nullable = false)
    private String content;

    @OneToMany(mappedBy = "post")
    private List<Comment> comments;
}
@Entity
@Table(name = "comment")
public class Comment {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "post_id", nullable = false)
    private Post post;

    @Column(length = 200, nullable = false)
    private String content;
}

post, comment 테이블을 엔티티로 각각 Post, Comment 로 표현했다.

두 엔티티는 1:N 양방향 연관관계를 맺는다.


Comment 엔티티의 @JoinColumn 어노테이션의 name 속성 값으로 post_id 를 설정했다. comment 테이블에서 post 테이블의 id 컬럼을 조회할 외래키를 post_id 로 설정했다. name 속성 값은 이 post_id 를 가리킨다.


위 예시에서 Comment 엔티티의 @JoinColumn 어노테이션에 referencedColumnName 을 작성하지 않았지만 만약에 작성한다면 id 로 작성하면 된다. post 테이블의 pk 컬럼명이 id 다.



<참고>

https://ko.wikipedia.org/wiki/%EC%99%B8%EB%9E%98_%ED%82%A4

https://jakarta.ee/specifications/persistence/3.1/apidocs/jakarta.persistence/jakarta/persistence/joincolumn

https://velog.io/@beomdrive/JPA-%EB%A7%A4%ED%95%91-%EA%B8%B0%EC%B4%88-JoinColumn

'Dev > Database' 카테고리의 다른 글

트랜잭션 격리 수준  (0) 2023.08.25
Clustered Index & Non Clustered Index  (0) 2023.05.10
TypeORM - getMany vs getRawMany  (0) 2023.02.21

트랜잭션 격리 수준

트랜잭션의 격리 수준(isolation level) 이란 여러 트랜잭션이 동시에 처리될 때 특정 트랜잭션이 다른 트랜잭션에서 변경하거나 조회하는 데이터를 볼 수 있게 허용할지 말지를 결정하는 것이다.


트랜잭션 격리 수준은 서로 다른 트랜잭션이 같은 데이터에 접근할때 데이터를 처리하는 방법과 관련이 있다.


격리 수준은 READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE 4가지로 나눌 수 있다.

격리 수준에 따라 발생할 수 있는 문제가 달라지는데, 이 문제는 Dirty Read, Non Repetable Read, Phantom Read 3가지가 있다.


Dirty Read Non Repetable Read Phantom Read
READ UNCOMMITTED O O O
READ COMMITTED X O O
REPETABLE READ X X O (MySQL InnoDB, PostgreSQL 는 X)
SERIALIZABLE X X X

격리 수준

글에서 언급할 트랜잭션 A, 트랜잭션 B 는 트랜잭션을 구분하기 위해 임의로 정한 트랜잭션 이름이다.


READ UNCOMMITTED

트랜잭션 A 에서 변경한 내용을 commit 여부와 관계없이 트랜잭션 B 에서도 볼 수 있는 격리 수준이다. 이 현상을 Dirty Read 라고 한다.


Dirty Read 뿐만 아니라 Non Repeatable Read 와 Phantom Read 도 발생한다.



READ COMMITTED

트랜잭션 A 에서 commit 한 데이터만 트랜잭션 B 에서 볼 수 있으나 트랜잭션 B 에서 동일한 데이터를 조회할 때 결과가 서로 다른 Non Repeatable Read 문제가 발생할 수 있다. Non Repeatable Read 는 Update 쿼리와 연관해서 생각해볼 수 있다.


트랜잭션 A 가 시작하고 한 데이터를 update 했으나 commit 을 아직 하지 않은 상태에서 트랜잭션 B 가 동일한 데이터를 조회하면 A 가 update 하기 전의 결과가 조회된다. 그러나 A 가 commit 을 하고나서 B 의 트랜잭션이 끝나기 전에 다시 B 가 동일한 데이터를 조회하면 A 가 update 한 결과가 조회된다.


한 트랜잭션 안에서 동일한 데이터를 조회할 때 서로 다른 결과가 나올 수 있다.


Dirty Read 는 발생하지 않으나 Non Repeatable Read 뿐만 아니라 Phantom Read 도 발생한다.



REPEATABLE READ

앞에서 언급한 Dirty Read, Non Repeatable Read 문제는 발생하지 않으나 REPEATABLE READ 격리 수준에서는 Phantom Read 문제가 발생할 수 있다.


단, MySQL 의 InnoDB 와 PostgreSQL 에서는 REPEATABLE READ 격리 수준에서 Phantom Read 문제가 발생하지 않는다. MySQL InnoDB 은 디폴트 격리 수준으로 REPETABLE READ 를 사용한다.


Phantom Read 는 이름에서 느껴지듯 다른 트랜잭션의 변경에 의해 트랜잭션 내에서 데이터가 마치 유령처럼 생겼다 없어졌다 하는 현상이 발생한다. Phantom Read 는 Insert 쿼리와 연관지어 생각해볼 수 있다.


트랜잭션 A 에서 Insert 쿼리로 테이블에 (Idx: 2, Name: Steve Jobs) row 를 추가한다고 할때 insert 쿼리가 발생하기 전에 트랜잭션 B 에서 트랜잭션을 시작하고 이 테이블의 모든 row 를 조회했을 때는 (Idx: 2, Name: Steve Jobs) 가 보이지 않는다.


그러나 A 의 insert 가 발생하고 commit 까지 끝나고 나서 B 의 트랜잭션이 끝나기 전에 다시 이 테이블의 모든 row 를 조회했을 때 이전에 없었던 (Idx: 2, Name: Steve Jobs) 가 보이게 되는 현상이 Phantom Read 다.




SERIALIZABLE

가장 엄격한 격리 수준으로 Dirty Read, Non Repeatable Read, Phantom Read 문제가 발생하지 않는다. 그만큼 동시 처리 성능은 떨어지지만 데이터 정합성이 높다.


<참고>

Real MySQL 8.0 1권

https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html

https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_consistent_read

https://www.postgresql.kr/blog/pg_phantom_read.html

https://www.postgresql.org/docs/current/transaction-iso.html#XACT-READ-COMMITTED

https://hudi.blog/transaction-isolation-level/

https://learn.microsoft.com/en-us/sql/t-sql/statements/set-transaction-isolation-level-transact-sql?view=sql-server-ver16

'Dev > Database' 카테고리의 다른 글

JPA - @JoinColumn  (0) 2024.04.11
Clustered Index & Non Clustered Index  (0) 2023.05.10
TypeORM - getMany vs getRawMany  (0) 2023.02.21

클러스터형 인덱스 & 비클러스터형 인덱스

클러스터 인덱스와 비클러스터 인덱스에 대해서 정리를 해보려고 한다. MySQL, SQL Server 관련 자료를 중심으로 작성했다.


Page

클러스터형, 비클러스터형 인덱스는 B-tree 자료구조를 바탕으로 저장된다. B-tree 자료구조의 각 노드들은 페이지 단위로 관리된다. 페이지는 MySQL 의 경우 16KB 가 디폴트 크기다. MySQL 5.6 버전 이상부터는 페이지 크기를 innodb_page_size 변수 설정으로 변경할 수 있다.

page 는 SQL Server 에서 데이터 저장의 가장 기본 단위다. MySQL 에서는 InnoDB 엔진이 디스크와 메모리 간의 데이터를 전달할 수 있는 최소 단위다.

디스크 저장공간은 file 에 할당되는데 file 은 논리적으로 page 단위로 구분된다.


예시 테이블 정보

아래 이미지로 나타낼 데이터들의 가상 테이블 정보는 다음과 같다.

테이블명은 Company 이며 컬럼은 name 과 founder 로 구성된다. 10개의 데이터가 들어있다.


클러스터형 인덱스란?

데이터가 저장된 페이지 자체를 정렬하는 기준이 되는 인덱스다. 테이블 당 하나만 설정할 수 있다.

PRIMARY KEY 를 지정하면 MySQL (의 InnoDB 엔진)이나 SQL Server 는 자동으로 해당 키를 클러스터형 인덱스로 지정한다.

테이블에 PRIMARY KEY 가 없으면 UNIQUE 제약 조건(MySQL 은 모든 컬럼들 중 NOT NULL 로 정의된 첫번째 UNIQUE 컬럼) 이 지정된 컬럼을 클러스터형 인덱스로 설정한다.

만약에 PRIMARY KEY, UNIQUE 제약 조건 모두 없다면 MySQL 은 GEN_CLUST_INDEX 라는 row ID 값을 갖는 가상의 6 바이트 크기의 컬럼을 생성하여 클러스터형 인덱스로 설정한다. row ID 는 데이터 행이 insert 된 순서대로 증가한다.


위는 Company 테이블의 PRIMARY KEY 로 name 컬럼이 지정된 경우다.

클러스터형 인덱스는 B-tree 의 루트 페이지와 리프 페이지를 중심으로 살펴볼 수 있다. PRIMARY KEY 를 기준으로 물리적으로 데이터가 저장된 데이터 페이지 자체를 정렬한다.

루트 페이지에는 리프 페이지의 번호와 각 리프 페이지의 첫번째 데이터 정보를 갖는다. 리프 페이지는 실제로 데이터가 저장되어 있는 데이터 페이지와 같다.


비클러스터형 인덱스

위는 Company 테이블의 인덱스로 founder 가 지정된 경우다. PRIMARY KEY 는 없다.

비클러스터형 인덱스 SECONDARY INDEX(보조 인덱스) 라고도 하며 클러스터형 인덱스와 달리 물리적으로 저장된 데이터를 정렬하지 않는다.

비클러스터형 인덱스의 리프 페이지는 데이터 페이지와 다르다. 클러스터형 인덱스와 달리 데이터 페이지 자체도 정렬되어 있지 않은 힙 형태로 구성된다.

비클러스터형 인덱스는 한 테이블에 인덱스를 여러개 설정할 수 있고, 설정한 인덱스를 기준으로 정렬한 인덱스 정보를 갖는다.

루트 페이지는 리프 페이지의 페이지 번호와 각 페이지의 첫번째 데이터 정보를 갖는다. 리프 페이지는 인덱스로 설정한 키와 해당 키를 포함한 데이터가 저장된 데이터 페이지의 위치 정보를 갖는다. 이 위치 정보는 RID 라고 하며 파일(file) 번호, 데이터 페이지(page) 번호, 행(row) 번호로 구성된다.


클러스터형 인덱스 + 비클러스터형 인덱스

위는 Company 테이블의 name 컬럼이 PRIMARY KEY 로 지정 되었고, founder 컬럼이 SECONDARY INDEX 로 지정된 경우다.

클러스터형 인덱스와 비클러스터형 인덱스가 함꼐 구성된 경우는 클러스터형 인덱스로 사용되는 PRIMARY KEY 와 함께 별도로 비클러스터형 인덱스를 보조 인덱스로 설정한 경우다.

MySQL 에서 모든 비클러스터형 인덱스는 클러스터형 인덱스의 정보를 갖는다. 그래서 클러스터형 인덱스의 크기가 커지면 비클러스터형 인덱스의 크기도 커지게 된다.


비클러스터형 인덱스의 인덱스 행에서 데이터 행으로의 포인터를 행 로케이터라고 합니다. 행 로케이터의 구조는 데이터 페이지가 힙에 저장되는지 아니면 클러스터형 테이블에 저장되는지에 따라 다릅니다. 힙의 경우 행 로케이터는 행에 대한 포인터입니다. 클러스터형 테이블의 경우 행 로케이터는 클러스터형 인덱스 키입니다.


테이블에 비클러스터형 인덱스만 있을 경우에는 비클러스터형 인덱스의 리프 페이지는 RID 정보를 갖고 있고 이 RID 를 기준으로 데이터 페이지를 조회한다. 테이블에 비클러스터형 인덱스 뿐만 아니라 클러스터형 인덱스도 있을 경우에는 비클러스터형 인덱스는 클러스터형 인덱스 (컬럼) 값을 갖고 있고 클러스터형 인덱스를 기준으로 데이터 페이지에 접근한다.


<참고>

https://dev.mysql.com/doc/refman/8.0/en/innodb-index-types.html

https://learn.microsoft.com/ko-kr/sql/relational-databases/indexes/clustered-and-nonclustered-indexes-described?view=sql-server-ver16

https://learn.microsoft.com/ko-kr/sql/relational-databases/indexes/create-clustered-indexes?source=recommendations&view=sql-server-ver16

https://hudi.blog/db-clustered-and-non-clustered-index/

https://velog.io/@gillog/SQL-Clustered-Index-Non-Clustered-Index

https://pangtrue.tistory.com/286

https://dev.mysql.com/doc/refman/8.0/en/innodb-physical-structure.html

https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_page

https://learn.microsoft.com/en-us/sql/relational-databases/pages-and-extents-architecture-guide?view=sql-server-ver16

https://ask.sqlservercentral.com/questions/39281/what-is-a-rid-lookup.html

'Dev > Database' 카테고리의 다른 글

JPA - @JoinColumn  (0) 2024.04.11
트랜잭션 격리 수준  (0) 2023.08.25
TypeORM - getMany vs getRawMany  (0) 2023.02.21

getMany vs getRawMany

There are two types of results you can get using select query builder: entities and raw results. Most of the time, you need to select real entities from your database, for example, users. For this purpose, you use getOne and getMany. However, sometimes you need to select specific data, like the sum of all user photos. Such data is not an entity, it's called raw data. To get raw data, you use getRawOne and getRawMany.


TypeORM 공식문서를 보면 getMany, getRawMany 의 차이점이 나오긴 하는데, 이를 보고 오해한 부분이 있었다. getRawMany 는 sum 등의 함수로 컬럼 값을 가공할 경우에 사용하는 것이라고 생각했다.


그러다 query builder 로 left join 을 하여 getMany 로 select 를 할 경우 원하는 결과물이 나오지 않는다는 회사 동료분의 얘기를 듣게 됐다. 게다가 getRawMany 로는 원하는 결과물이 나오자 둘의 차이가 더욱 궁금해졌다.


소스코드

0.3.12 버전 기준


getMany

getMany 는 쿼리한 결과물을 entity 로 매핑하여 리턴한다.

getMany 와 연관된 주요 함수는 getRawAndEntities, executeEntitiesAndRawResults 다.


// typeorm/src/query-builder/SelectQueryBuilder.ts

async getMany(): Promise<T[]> {}

async getRawAndEntities<T = any>(): Promise<{
  entities: Entity[]
  raw: T[]
}> {}

protected async executeEntitiesAndRawResults(
  queryRunner: QueryRunner,
): Promise<{ entities: Entity[]; raw: any[] }> {}

일부 코드를 조금 더 살펴 보겠다.


// typeorm/src/query-builder/SelectQueryBuilder.ts

async getMany<T = any>(): Promise<T[]> {
  if (this.expressionMap.lockMode === "optimistic")
    throw new OptimisticLockCanNotBeUsedError()

  // getRawAndEntities 는 entities 와 raw 를 리턴하고
  const results = await this.getRawAndEntities()
  // getMany 에서는 raw 를 제외하고 entities 만 리턴한다
  return results.entities
}

async getRawAndEntities<T = any>(): Promise<{
  entities: Entity[]
  raw: T[]
}> {
  try {
    ...
    const results = await this.executeEntitiesAndRawResults(queryRunner)
    ...
    return results;
  } catch {

  } finally {

  }
}

protected async executeEntitiesAndRawResults(
  queryRunner: QueryRunner,
): Promise<{ entities: Entity[]; raw: any[] }> {
  let rawResults: any[] = [],
      entities: any[] = []

  // 아래 if 조건은 이해하지 못했다
  // 조건문에 따라서 rawResults 를 얻는 방법이 달라진다
  if (
    (this.expressionMap.skip || this.expressionMap.take) &&
     this.expressionMap.joinAttributes.length > 0
  ) {
    // getRawMany 를 호출한다
    rawResults = await new SelectQueryBuilder(
      this.connection,
      queryRunner
    )
      .select()
        ...
      .getRawMany();
  } else {
    // loadRawResults 를 호출한다.
    rawResults = await this.loadRawResults(queryRunner)
  }

  if (rawResults.length > 0) {
    // RawSqlResultsToEntityTransformer 클래스의
    // transform 함수에서
    // rawResults 를 entity 로 매핑하는 작업을 수행한다
    const transformer = new RawSqlResultsToEntityTransformer(
      this.expressionMap,
      this.connection.driver,
      rawRelationIdResults,
      rawRelationCountResults,
      this.queryRunner,
    )
    entities = transformer.transform(
      rawResults,
      this.expressionMap.mainAlias!,
    )
  }

  // raw 의 값으로 rawResults,
  // entities 의 값으로 entities 를 
  // 설정하여 객체 형태로 리턴
  return {
    raw: rawResults,
    entities: entities,
  }
}

getRawMany

getRawMany 의 주요 함수는 loadRawResults 이고, getMany 에서도 조건에 따라서 loadRawResults 를 호출하기도 한다.


// typeorm/src/query-builder/SelectQueryBuilder.ts

async getRawMany<T = any>(): Promise<T[]> {
  try {
    ...
    const results = await this.loadRawResults(queryRunner)
    ...
    return results;
  } catch {

  } finally {

  }
}

protected async loadRawResults(queryRunner: QueryRunner) {
  const [sql, parameters] = this.getQueryAndParameters()
  ...
  const results = await queryRunner.query(sql, parameters, true)
  ...
  return results.records;
}

리턴 타입

TypeORM 소스코드를 보면 getMany 와 getRawMany 의 리턴 타입이 서로 다르다.

getMany 는 Promise< Entity[] > 를 리턴하고

getRawMany 는 Promise< T[] > 를 리턴한다.


async getMany(): Promise<Entity[]> {}
async getRawMany<T = any>(): Promise<T[]> {}

MyService 라는 서비스에서 MyList 엔티티를 YourList 와 left join 을 수행하여 getMany 를 통해 리턴하려고 한다.


@Injectable
export class MyService {
  constructor(
    @InjectRepository(MyList)
    private readonly myListRepository: Repository<MyList>,
  ) {}

  async getMyListTest(num: number) {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .getMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

getMany 가 나타내는 리턴 타입은 Promise< MyList[] > 다.

left join 으로 어떤 테이블을 하는지와 상관없이 left join 에 활용된 엔티티의 내용은 확인할 수 없다.


이번에는 getMany 대신 getRawMany 를 사용한다.


@Injectable
export class MyService {
  constructor(
   @InjectRepository(MyList)
   private readonly myListRepository: Repository<MyList>,
  ) {}

  async getMyListTest(num: number) {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .getRawMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

getRawMany 가 나타내는 리턴 타입은 Promise< any[] > 다.


Select

select 로 MyList 엔티티의 변수를 지정할 수 있다.


@Injectable
export class MyService {
  constructor(
    @InjectRepository(MyList)
    private readonly myListRepository: Repository<MyList>,
  ) {}

  async getMyListTest(num: number): Promise<MyList[]> {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .select(['MyList.ListID', 'YourList.ListID'])
        .getMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

다만 이때 getMany 는 ListID 외에 select 에 포함하지 않은 MyList 엔티티의 다른 컬럼 값에도 접근이 가능하다.

이때 실제로는 select 에 포함되지 않아서 undefined 가 나온다.

그리고 left join 에 활용한 YourList 엔티티의 컬럼은 결과물에 담기지 않는다.


MyList 엔티티 클래스는 아래와 같다.


@Entity('MyList')
export class MyList {
  @PrimaryGeneratedColumn({
    type: 'int',
  })
  ListNo: number;

  Column({
    type: 'varchar',
  })
  ListID: string;

  Column({
    type: 'varchar',
  })
  ListName: string;
}

YourList 엔티티 클래스는 아래와 같다.


@Entity('YourList')
export class YourList {
  @PrimaryGeneratedColumn({
    type: 'int',
  })
  ListNo: number;

  Column({
    type: 'varchar',
  })
  ListID: string;

  Column({
    type: 'varchar',
  })
  ListName: string;
}

MyService 에서 test 함수를 수행했다.

test 함수에서는 getMany 로 쿼리 결과를 리턴한다.


@Injectable
export class MyService {
  constructor(
    @InjectRepository(MyList)
    private readonly myListRepository: Repository<MyList>,
  ) {}

  async test() {
    const result: MyList[] = await this.getMyListTest(1);

    // 출력 결과는 아래와 같다
    // [MyList {ListID: 1}]
    //
    // 이 결과를 받게되는 터미널 등의 클라이언트는
    // MyList 엔티티를 알지 못하기 때문에
    // [{ListID: 1}]
    console.log(result);

    // select 에 포함되지 않은 ListName 에 접근
    // undefined 가 출력된다
    console.log(result[0].ListName)
  }

  async getMyListTest(num: number) {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .select(['MyList.ListID'])
        .getMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

test 함수에서 getMyListTest 함수를 호출한 결과물에 접근하는데 이때 IDE 는 result[0]. 까지 입력하면 MyList 에 관련된 모든 컬럼들을 자동 완성 후보로 보여준다.

타입스크립트는 result[0] 에 select 한 ListID 만 있는 것을 알지 못하고 MyList 엔티티의 모든 컬럼들에 접근하려고 한다. result[0].ListName 의 출력을 시도하면 undefined 가 나온다.


MyService 에서 test 함수를 수행하는데 이번에는 getRawMany 로 쿼리 결과를 리턴한다.

getRawMany 는 배열 안에 객체 형태로 결과값이 담긴다.


@Injectable
export class MyService {
  constructor(
    @InjectRepository(MyList)
    private readonly myListRepository: Repository<MyList>,
  ) {}

  async test() {
    const result: any[] = await this.getMyListTest(1);
    // 결과물의 타입이 엔티티가 아니라서
    // 출력과 클라이언트는 모두 같은 결과물을 받게 된다
    // [{ListID: 1}]
    console.log(result);
    // result 에 대해 IDE 는 정확한 자동 완성을 할 수 없다
    // getMany 는 select 하지 않은 컬럼도 자동 완성으로 보여준다면
    // getRawMany 는 결과물이 무엇인지 실행하기 전까지 알 수가 없어서 IDE 는 정확한 자동 완성을 할 수 없다
    console.log(result[0].???)
  }

  async getMyListTest(num: number): Promise<any> {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .select(['MyList.ListID'])
        .getRawMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

위와 달리 아래는 select 없이 getRawMany 를 수행한다.


@Injectable
export class MyService {
  constructor(
    @InjectRepository(MyList)
    private readonly myListRepository: Repository<MyList>,
  ) {}

  async test() {
    const result: any[] = await this.getMyListTest(1);
    // YourList 엔티티의 컬럼은 모두 null 로 나옴에 주의
    //
    // [
    //    {
    //      MyList_ListNo: 1,
    //      MyList_ListID: 'my id',
    //      MyList_ListName: 'my name',
    //      YourList_ListNo: null,
    //      YourList_ListID: null,
    //      YourList_ListName: null,
    //    }
    // ]
    console.log(result);
  }

  async getMyListTest(num: number): Promise<any> {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .getRawMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

select 를 따로 하지 않으면 MyList, YourList 컬럼 값들이 모두 나오는데 다만 YourList 컬럼은 null 로 나온다.


alias

getMany 에서 alias 는 적용할 수 없다.


@Injectable
export class MyService {
  constructor(
    @InjectRepository(MyList)
    private readonly myListRepository: Repository<MyList>,
  ) {}

  async test() {
    const result: MyList[] = await this.getMyListTest(1);
    // 빈 배열이 출력된다
    // []
    console.log(result)
  }

  async getMyListTest(num: number) {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .select(['MyList.ListID AS listID'])
        .getMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

getRawMany 는 alias 로 설정한 값으로 나온다.


@Injectable
export class MyService {
  constructor(
    @InjectRepository(MyList)
    private readonly myListRepository: Repository<MyList>,
  ) {}

  async test() {
    const result: MyList[] = await this.getMyListTest(1);
    // alias 로 설정한 listID 가 나온다
    // [{listID: 1}]
    console.log(result)
  }

  async getMyListTest(num: number) {
    try {
      return await this.myListRepository
        .createQueryBuilder('MyList')
        .leftJoinAndSelect(YourList, 'YourList', 'MyList.ListNo = YourList.ListNo')
        .where('MyList.ListNo = :ListNo', { ListNo: num })
        .select(['MyList.ListID AS listID'])
        .getRawMany();
    } catch (err) {
      throw new InternalServerErrorException('잘못된 요청입니다', err);
    }
  }
}

정리

getMany 는 특정 컬럼을 select 하는게 아니라 엔티티 클래스 전체의 결과물을 얻을 때 사용하면 적절할 것 같다. 물론 특정 컬럼만 select 할 때도 가능하지만 이때는 select 하지 않은 컬럼에 접근하지 않도록 주의해야 한다.

특정 컬럼을 제외하고 싶다면 class-validator 등을 이용해서 별도의 클래스 인스턴스로 변환해주는 작업을 수행할 수도 있다. 저도 이에 대한 추가 학습이 필요합니다.


getRawMany 는 특정 컬럼만 select 할 때 사용하면 적절할 것 같다. join 하는 엔티티의 컬럼도 select 할 수 있고 alias 지정도 가능하다. 다만 엔티티 클래스 전체를 활용할 때는 select 를 하지 않으면 join 에 사용된 엔티티의 값은 null 로 나옴에 주의해야 한다.


참고

https://typeorm.io/select-query-builder#getting-raw-results

https://seungtaek-overflow.tistory.com/19

https://github.com/typeorm/typeorm/blob/74f7f796aa1d5d241687197f504d2786bee271e1/src/query-builder/SelectQueryBuilder.ts#L1747

https://jojoldu.tistory.com/610

'Dev > Database' 카테고리의 다른 글

JPA - @JoinColumn  (0) 2024.04.11
트랜잭션 격리 수준  (0) 2023.08.25
Clustered Index & Non Clustered Index  (0) 2023.05.10

+ Recent posts