[FP&&TIL] TypeORM의 Join, Relation, Active Record, Data Mapper

ytwicey·2021년 1월 17일
3

TIL

목록 보기
23/23
post-thumbnail

Cominciamo,

<사진은 전혀 상관없음. 그냥 멋있어서 넣어봤다.>
이번에 타입스크립트를 쓰면서 typeORM도 써보기로 했다. 익숙했던 sequelize를 떠나, 새로움을 찾아보기로 한 것이다. 데이터베이스 종류자체도 nosql로 해볼까 했는데, 너무 무리한 도전인가 싶어서 기존에 쓰던 mysql을 쓰기로 했다. (물론 데이터들 간의 관계성이 있기 때문이기도 함).
TypeORM을 쓰면서, 어려웠던 부분은 매우매우 매니하지만, 그중에서도 복잡하고 어려웠던 부분들을 골라서 한번 포스팅 해봐야지. 사실 블로그 기록들이 많지 않고, 전부 공식문서만 그대로 가져온 것들이라서 이해하기까지 시간이 오래걸렸는데, 다음에 내 블로그를 보는 사람들은 좀 쉬웠으면 좋겠다는 마음을 담아본다...하하








1. Connection

  • DB를 연결하는 것은 공식문서에도 나와있는데, typeorm init이라는 command line을 작성하면, 바로 User entity와 connection을 위한 코드가 app.ts(or index.ts)에 작성이 된다.
  • 진짜 신기했던 건, 시퀄라이즈는 매번 entity가 바뀔 때마다 migration을 해줘야 한다고 배웠는데, typeORM은 그냥 DB를 다시 연결해주면 변경사항이 DB에 적용이 되었다. (물론 마이그레이션이 있긴 있지만, 굳이 필요없는 느낌!?) 그냥 entity에서 바로 수정하고 연결하면 수정사항이 바로 적용돼서 너무 편했다. (그냥 내가 마이그레이션 하는 방법을 모르는 걸수도...)
  • ormconfig.json 작성도 필수!

2. Relation

  • 언제나 언제나 관계가 너무 어려운데, typeORM은 관계를 컬럼처럼 데코레이션을 이용해서 표현한다.
  • N:N은 @ManyToMany와 @JoinTable로 표현하고, 두 entity중에 한 쪽에만 작성해줘도 된다.
    그러면 자동으로 두 entity간의 테이블이 DB에 생성된다.
  • 1:M, M:1은 @OneToMany, @ManyToOne와 @JoinColumn으로 표현하면 된다.
    그러면 이것도 자동으로 FK와 컬럼이 테이블에 형성이 된다.

다대다 관계에서 가장 많이 참고 했던 블로그는 <TypeOrm의 ManyToMany 와 JoinTable 사용하기> 여기!

3. BaseEntity와 getRepository

  • 처음 typeorm을 쓸 때, 대규모의 데이터를 다룰 때 좋다고 들었다. 그 이유가 뭘까 하고 봤더니 위 두 개 키워드 때문이었다.

  • 우선 내가 이해한 바대로 적어보자면, BaseEntity는 Active Record를 위한 Entity 클래스다. 모든 entity들이 이 BaseEntity를 상속받을 수 있고, 상속을 하게 되면 BaseEntity를 위한 메소드를 entity들이 사용가능하다. 여기서 메소드라 함은 findOne 같은 쿼리문을 이야기 한다.

  • 기본적으로 mysql에서 다루는 쿼리문들이 있는데, 실제 mysql에서는 정해진 쿼리문을 써야하지만, sequelize 같은 경우에는 그러한 쿼리문을 정리해서 하나의 메소드로 쓸 수 있게 api를 만들어놨다.
    그러나 typeORM같은 경우는 이러한 쿼리문들을 이용해서 나만의 메소드를 만들 수 있는 것이 특징인데, 사실 많이 쓰는 것들은 보편적인 메소드로 지정해주면 사용하기 편할 거다. 그래서 그런 메소드들을 BaseEntity에 지정해두고, 사용할 수 있게 한다. 그런데 종류가 몇가지 안되기 때문에, 그리고 데이터마다 혹은 로직마다 필요한 메소드는 다르기 때문에 사실상 쿼리문으로 나만의 메소드를 작성하게 된다. 따라서... 쓰다보면 기본 메소드를 사용하는 일이 별로 없다.

  • BaseEntity 같은 경우는 그래서 Entity 파일에서 static을 이용해 메소드를 작성하고, 그것을 컨트롤러에서 불러다 쓸 수 있다.

  • 그러나 데이터가 크고, entity의 컬럼이 많고, 따라서 사용되는 메소드도 많다면?
    이럴 때 Data Mapper pattern을 사용한다.

    you define all your query methods in separate classes called "repositories", and you save, remove, and load objects using repositories.

공식문서에서는 data mapper pattern을 이렇게 설명하는데, 메소드를 분리된 클래스들에 정의하면, 그 클래스를 레포지토리라고 부른다. 따라서 메소드를 다른데다가 써놓고, 그것을 getRepository로 불러다 쓴다는 이야기다. 그러니까 아래 예시를 보면,

const timber = await User.findByName("Timber", "Saw");

여기서 User는 Entity다. 이 entity에 테이블 정보뿐만 아니라, 아래와 같은 메소드를 작성해 두었기 때문에,

 static findByName(firstName: string, lastName: string) {
        return this.createQueryBuilder("user")
            .where("user.firstName = :firstName", { firstName })
            .andWhere("user.lastName = :lastName", { lastName })
            .getMany();
    }

컨트롤러에서 위와 같이 불러서 쓸 수 있다. 그러니까 이 방식은 Active Record 방식으로 적혀진 로직이다.

그러나 data mapper는 조금 다른데,

const userRepository = connection.getCustomRepository(UserRepository);
const timber = await userRepository.findByName("Timber", "Saw");

우선 userRepository라고 하는 작성된 클래스가 있고, 거기서 findByName 이라고 하는 메소드를 불러온다.

import {EntityRepository, Repository} from "typeorm";
import {User} from "../entity/User";

@EntityRepository()
export class UserRepository extends Repository<User> {

    findByName(firstName: string, lastName: string) {
        return this.createQueryBuilder("user")
            .where("user.firstName = :firstName", { firstName })
            .andWhere("user.lastName = :lastName", { lastName })
            .getMany();
    }

}

그 userRepository는 이렇게 생겼다. 위의 active record와 다르게, 테이블에 관한 내용은 없고 오로지 메소드만 지정되어 있는 별도의 파일이라고 생각하면 된다.
그래서 entity와 메소드, 적용하기 위한 컨트롤러를 나눠서 작성할 수 있고, 코드를 심플하게 만들 수 있기 때문에 대용량의 데이터를 다룰 때 편리하다고 하는 것 같다.

  • 처음엔 이 부분이 이해가 안되어서 도대체 왜 이렇게 쓰는지 알 수가 없었는데, 막상 쓰고 나니까 컨트롤러가 간단해져서 보기도 좋고, 재사용성도 좋았다. 실제로 여기저기서 필요하면 불러다 쓸수 있다는 아주 커다란 장점이 있었다!!!!

4. JOIN

  • 해맸던 Join....
  • 이미 mysql의 쿼리문을 작성하는 건 어렵지 않았는데, typeorm을 이용해서 일반적인 CRUD를 작성하는 것도 어렵지 않았는데, leftJoin/innerJoin등의 조인 쿼리에 대한 내용은 처음에 찾기가 어려웠다. 더불어 다대다 테이블과 일대다 혹은 다대일의 테이블마다 데코레이션을 통한 @JoinColumn, @JoinTable이 끝이 아니라, 실제 이 컬럼과 테이블에 값을 삽입해줘야 하는데, 이 Join에 대한 쿼리문 및 접근법이 조금 다르다.

M:N 테이블

우선 프로젝트에서 하고 있는 데이터는 유저와 그룹(포스트)의 관계가 다대다이다.
그렇기 때문에 테이블에 데코레이션으로 @ManyToMany를 작성하는 것 외에도, 실제로 메소드로 사용하기 위해서는 아래와 같이 이렇게 조인테이블을 형성해줘야 했다. 그래야 postid와 userid로 구성된 테이블에 값이 들어갔다.

static JoinTheTable(postid: any, userid: any) {
    return this.createQueryBuilder()
      .relation(Post, "user")
      .of(postid)
      .add(userid);
  }

1:N, N:1 테이블

그리고 유저와 댓글의 관계는 1:N이기 때문에 아래와 같이 다시 조인컬럼에 대한 메소드를 작성해줘야했다.
그리고 이러한 메소드는 post작업으로 인해, commentid가 생성되고 나서, 관계를 형성할 수 있도록 로직이 작성되어야 했다.

static joinUser(id: any, userid: any) {
        return this.createQueryBuilder()
            .relation(Comment, "user")
            .of(id)
            .set(userid);
    }

뿐만 아니라, 1:N, N:1의 경우는 값을 불러올 때, left, inner등의 조인 쿼리문을 작성해야 했다.

static getComments(postid: string) {
        return this.createQueryBuilder("comment")
            .innerJoinAndSelect("comment.post", "post")
            .where("post.id = :id", { id: postid })
            .getMany()
        // mysql> select * from comment inner join post on comment.post_id = post.id;
    }
이번 typeorm으로 쿼리문을 작성할 때는 위의 주석과 같이 실제 mysql 쿼리문으로 작성한 뒤, typeorm으로 옮기는 경우가 많았다. 두 가지의 query문을 작성해볼 수 있는 좋은 기회였다.

innerJoin 경우를 예로 들면, 내장 메소드가 있었는데, 모든 조인은 innerJoinAndSelect / innerJoin로 나뉜다. 이 차이점은 다음 링크를 확인 >> TypeORM으로 Join 하기






Finiamo,

typeorm을 이용한 로직 작성은 이제 거의 끝났고, 마무리 작업만 남았다. 사실 공부하면서 재밌었다. 쿼리를 이용해서 메소드를 직접 customize하는 작업도 생각보다 좋았다. typescript를 이용한 작성이라 걱정이되긴 했는데, @types/ 이런 모듈 만든 사람들은 정말 칭찬해... 최대한 애니스크립트를 만들고 싶지 않아서, 타입을 많이 지정해주려고 했는데, 사실 마음 한 켠에선 제대로 못쓰고 있는 것 같아 아쉽다.
백엔드의 가장 큰 희열은 생각한 로직이 제대로 적용되어서 서버가 잘 돌아가고, 데이터가 잘 들어가고 나올 때인 것 같다. 물론 백엔드의 세상은 무궁무진하고 나로서는 발꼬락만 담궈본 거겠지만, 아직까지는 재밌다.
내일부터는 websocket을 하게 될 것 같은데, 그것도 재밌을 것 같아 기대된다. 파이널 화이팅.

profile
always 2B#

0개의 댓글