typeORM

설명
typeorm대신 sequalize를 써도 된다.
Tags
DB
 

 
 
 

개요

sql을 직접 작성해서 db와 상호작용 할 수도 있지만,
TypeORM을 통하면 코드를 통해 db와 상호작용 할 수 있다.

설치

npm install --save @nestjs/typeorm typeorm pg # postgresql(쓰고자 하는 db 쓰면 됨) # e.g. # npm install --save @nestjs/typeorm typeorm mysql

NestJS 연동

DB 연결정보 등의 typeorm 설정은 ormconfig.json파일로 할 수도 있고, 코드에 바로 작성할 수도 있다.
아래는 코드로 설정하는 예시
@Module({ imports: [ TypeOrmModule.forRoot({ type: 'postgres', host: 'localhost', port: 5432, username: 'haneojin', password: '12345', // localhost의 경우 비번 틀려도 됨. database: 'nuber-eats', synchronize: true, logging: true, // 콘솔에 출력 }), ... ], controllers: [], providers: [], })
 
notion image
postgresql port 확인 방법은 위와 같다.
postgresql을 열고 server setting에서 port를 확인할 수 있다.
 

Entity

Entity is a class that maps to a database table (or collection when using MongoDB). You can create an entity by defining a new class and mark it with @Entity()
import { Entity, PrimaryGeneratedColumn, Column } from "typeorm" @Entity() export class User { @PrimaryGeneratedColumn() id: number @Column() firstName: string @Column() lastName: string @Column() isActive: boolean }
entity 예시
 
import { Field, ObjectType } from '@nestjs/graphql'; @ObjectType() export class Restaurant { @Field(() => String) name: string; @Field(() => Boolean, { nullable: true }) isVegan: boolean; @Field(() => String) address: string; @Field(() => String) ownerName: string; }
graphql object type
graphqlObjectType을 선언하는 것과 굉장히 유사하다.
ObjectType은 자동으로 graphql schema를 빌드하기 위해 사용하는 데코레이터다.
Entity 데코레이터TypeOrmDB에 이걸 저장할 수 있도록 해준다.
 

kind of decorators

 

데코레이터 중첩 (graphql, TypeORM)

데코레이터라서 중첩으로 쌓으면 된다.
graphql 스키마를 생성하기 위한 데코레이터와, TypeORM을 위한 DB에 저장될 실제 데이터 형식을 표현하는 데코레이터 모두 하나의 entity를 선언하면서 충족시킬 수 있다.
import { Field, ObjectType } from '@nestjs/graphql'; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm'; @ObjectType() // for graphql schema @Entity() // for TypeORM export class Restaurant { @Field(() => Number) // for graphql schema @PrimaryGeneratedColumn() // for TypeORM id: number; @Field(() => String) // for graphql schema @Column() // for TypeORM name: string; @Field(() => Boolean, { nullable: true }) // for graphql schema @Column() // for TypeORM isVegan: boolean; @Field(() => String) // for graphql schema @Column() // for TypeORM address: string; @Field(() => String) // for graphql schema @Column() // for TypeORM ownerName: string; }

synchronize

notion image
prod가 아닐 경우에는 synchronize=true로 설정하였으므로 개발 시에는 자동 싱크가 된다.
싱크할 entities를 따로 명시하였고,
entities가 변화되면 TypeORMsynchronize 옵션에 의해 자동으로 DB에 변경사항이 마이그레이션 되며 반영된다.
TypeORM synchronize 예시

Active Records vs Data Mapper

선택하면 되는데
  • Data Mapper
    • repository를 통해서 관리됨
      • 어디서든 접근할 수 있다. (앱 코드 내에서도, 테스트 코드에서도)
    • 대규모 앱에서 유용하다
  • Active Records
    • 소규모 앱에서 단순하게 사용할 수 있도록 도와줌
 
NestJS+TypeORM 에서 Repository를 사용할 수 있다.
repository를 활용하면 어디서든 접근할 수 있다. (테스트에서도 사용 가능)
그래서 Data Mapper 형태로 가는 것을 좀 더 추천함
 
 
// 블라블라 레포지토리 연동하는 과정
// …
notion image
graphql로 요청했을 때, 실제 DB에서 쿼리를 쳐서 결과를 반환하도록 된 것을 확인할 수 있다. 연동 성공!
 
 

Create, Save, Updat

  • create: 자바스크립트 단에서 단순히 인스턴스를 생성해줌 (DB는 건들지 않음)
  • save: DB에 반영함 (없다면 만들고, 있다면 업데이트함)
  • update: 항목을 업데이트함. - 첫번째 인자는 criteria인데, 업데이트 하고자 하는 항목을 특정하기 위함임. id를 넣어주면 searchById가 되겠지만 {name: "blah"} 와 같은 형태도 서칭도 가능
💡
update는 실제로 DB에 있는지 없는지 여부를 체크하지 않는다는 점에 유의!! update는 entity를 수정하는게 아니라 바로 db에 쿼리를 보냄. 가장 빠른 방식으로 최적화 되어있기 때문. 따라서 @BeforeUpdate와 같은 데코레이터가 동작해야한다면, entity로 수정 후 save를 이용해야 한다. - i.g. 패스워드 변경 시 해시화를 시킨 후 저장해야하는데, update를 통하면 @BeforeUpdate가 동작하지 않아서 패스워드가 해시화되지 않은 채 db에 평문으로 저장하도록 update query를 치게 됨.
createRestaurant( createRestaurantDto: CreateRestaurantDto, ): Promise<Restaurant> { const newRestaurant = this.restaurants.create(createRestaurantDto); // instance 생성 return this.restaurants.save(newRestaurant); // 실제 db에 저장 }
 

Entity와 Dto 동기화 (Mapped type)

entity에 필드가 수정됐으면, Dto에도 마찬가지로 수정이 되어야 한다.
하지만 까먹을 경우가 있다.
어떻게 까먹는 휴먼에러를 없애고, 쉽게 동기화 시킬 수 있을까?
 
이걸 위해 mapped type을 사용할 거다.
  • nestjs mapped type : https://docs.nestjs.com/techniques/validation#mapped-types
    • PartialType() : 전부 optional로 해서 새로운 걸 만듦
    • PickType() : 특정 부분만 선택해서 새로운 걸 만듦
    • OmitType() : 특정 부분만 제외하고 새로운 걸 만듦
    • IntersectionType() : 여러개를 합쳐서 새로운 걸 만듦
    • 인자 구성 : ( Base, key, decorator )
💡
InputType에 대해서만 동작하므로, InputType으로 만들거나 InputType으로 변환해서 사용해야 한다. mapped type의 세번째 인자를 통해 decorator를 바꿀 수 있도록 지원하고 있어서, 혹시나 첫번째 인자가 InputType이 아닌 경우에는 마지막에 InputType을 명시해서 decorator를 바꿔주면 정상적으로 동작한다.
⚠️
@nestjs/graphql이나 @nestjs/swagger를 쓰고 있다면 해당 라이브러리에 적합한 mapped type을 사용해야 한다. - mapped type of graphql ← 나는 graphql 쓰니까 이걸 써야함 - mapped type when using swagger
 

방법1

// dto file @InputType() export class CreateRestaurantDto extends OmitType( Restaurant, ['id'], // entity에서 id는 제외하고 만들고 싶음 InputType, // Restaurant가 InputType이 아니라 ObjectType이므로 세번째 인자를 통해 InputType으로 데코레이터를 변형시킨 후 mapped type이 동작하도록 해줘야 함. ) {} // entity file @ObjectType() // for graphql schema @Entity() // for TypeORM export class Restaurant { @Field(() => Number) // for graphql schema @PrimaryGeneratedColumn() // for TypeORM id: number; @Field(() => String) // for graphql schema @Column() // for TypeORM name: string; @Field(() => Boolean, { nullable: true }) // for graphql schema @Column() // for TypeORM isVegan: boolean; @Field(() => String) // for graphql schema @Column() // for TypeORM address: string; @Field(() => String) // for graphql schema @Column() // for TypeORM ownerName: string; }
RestaurantObjectiveType 데코레이터로 ObjectiveType이다.
하지만 DTO 객체는 InputType 이어야 한다.
이 때 MappedType의 세번째 옵셔널 인자인 decorator를 명시하여 다른 데코레이터가 적용되도록 덮을 수 있다. 그래서 dto 선언 시 extends를 하면서 InputType 데코레이터가 되도록 명시했다.
 

방법2 - 추천

💡
dto 파일 하나에서 다 처리하고 싶으므로, 개인적으로 이 방법을 추천. 1. graphql 2. typeORM 3. validation
@InputType() export class CreateRestaurantDto extends OmitType(Restaurant, ['id']) {} @InputType({ isAbstract: true }) @ObjectType() // for graphql schema @Entity() // for TypeORM export class Restaurant { @Field(() => Number) // for graphql schema @PrimaryGeneratedColumn() // for TypeORM id: number; @Field(() => String) // for graphql schema @Column() // for TypeORM name: string; @Field(() => Boolean, { nullable: true }) // for graphql schema @Column() // for TypeORM isVegan: boolean; @Field(() => String) // for graphql schema @Column() // for TypeORM address: string; @Field(() => String) // for graphql schema @Column() // for TypeORM ownerName: string; }
entity file에서 ObjectType으로 명시할 거지만, InputType을 abstract로 선언한다면,
dto 파일에서 decorators 란을 명시적으로 InputType으로 덮어쓰지 않아도 된다.
 
두 가지 방법이 있는 것인데, 선택은 자유

graphql, typeorm, validation 3가지를 entity 하나의 파일에서 하는 예시

@Field((type) => Boolean, { defaultValue: true }) // graphql 스키마에서 이 필드의 defaultValue가 true임을 의미 @Column({ default: true }) // database에서 이 필드의 defaultValue가 true임을 의미 @IsOptional() // validation : 이건 옵셔널일 수 있고, @IsBoolean() // validation : 값이 있다면 boolean 이어야 한다. isVegan?: boolean;