NestJS TYPEORM

LeeJaeHoon·2021년 12월 4일
0
post-thumbnail

Our FIRST Entity

Entity

Entity란 데이터베이스에 저장되는 데이터의 형태를 보여주는 모델 같은 것입니다.

typeorm을 통해 entity사용하기

새 클래스를 정의하고 @Entity()로 표시하여 엔터티를 만들 수 있습니다.

놀라운 것은 이전에 저희가 @ObjectType()으로 정의한 graphql의 schema와 같이 사용할 수 있다는 점입니다.

import { Field, ObjectType } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

//graphQL에서 사용하는 스키마를 자동으로 생성해주고 db에도 자동으로 즉시 반영해준다!
@ObjectType() //Restaurant을 위한 Object type을 만들어 준다.
@Entity() // typeorm이 db에 저장해준다
export class Restaurant {
  @PrimaryGeneratedColumn()
  @Field((type) => Number)
  id: number;

  @Field((type) => String) //@Field의 첫번째 argument로는 returnTypeFunction이 와야한다.
  @Column() //typeorm을 위한 것
  name: string;

  @Field((type) => Boolean, { nullable: true })
  @Column()
  isVegan?: boolean;

  @Field((type) => String)
  @Column()
  address: string;

  @Field((type) => String)
  @Column()
  ownerName: string;

  @Field((type) => String)
  @Column()
  catagoryName: string;
}
  • @PrimaryGeneratedColumn()은 위의 SQL과 비교했을 때 AUTO_INCREMENTPRIMARY KEY를 표시해주는데, 데이터베이스를 어떤 것(Postgres, MySQL 등)을 쓰냐에 따라 AUTO_INCREMENT, SERIAL, SEQUENCE 중에 지원되는 기술을 사용한다.
  • @Column은 각각 테이블의 필드가 됩니다.

app.module.ts

각 엔터티는 연결 옵션에 등록해야 합니다.

synchronize = true로 해주면 TypeOrm이 entity를 찾고 열어서 migration 해주는것,DB의 구성을 자동으로 바꿔줍니다.

entities: [Restaurant]와 같이 해주면 연결할 수 있습니다.

import { Module } from '@nestjs/common';
import * as Joi from 'joi';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { RestaurantsModule } from './restaurants/restaurants.module';
import { Restaurant } from './restaurants/entities/restaurant.entity';

@Module({
  imports: [
    ConfigModule.forRoot({
      ...
    }),
    TypeOrmModule.forRoot({
      type: 'postgres',
      host: process.env.DB_HOST,
      port: +process.env.DB_PROT,
      username: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
      //TypeOrm이 entity를 찾고 열어서 migration 해주는것,DB의 구성을 자동으로 바꿔준다.
      //production에서는 실제 데이터를 가지고 있으니까 DB를 수동으로 바꾸고 싶을 때가 있기 때문
      synchronize: process.env.NODE_ENV !== 'prod',
      logging: true,
      entities: [Restaurant],
    }),
    GraphQLModule.forRoot({
      autoSchemaFile: true, //메모리에 저장
    }),
    RestaurantsModule,
  ],
  controllers: [],
  providers: [],
})
export class AppModule {}

Data Mapper vs Active Record

Typescript를 이용해서 DB에 있는 Restaurant에 접근하는 방법은 두가지 방법이 있습니다.

Activ Record

모델 내에서 데이터베이스에 엑세스하는 접근 방식입니다.

Data Mapper

모델 대신 리포지토리 내의 데이터베이스에 액세스하는 접근 방식입니다.

둘중 어느 것을 선택해야 할까?

두 전략 모두 장단점이 있습니다. 소프트웨어 개발에서 항상 염두에 두어야 할 한 가지는 응용 프로그램을 유지 관리하는 방법입니다.

Data Mapper은 유지 관리에 도움이 되며, 이는 더 큰 앱에서 더 효과적입니다.

ActivRecord은 작은 앱에서 잘 작동하는 작업을 단순하게 유지하는 데 도움이 됩니다. 그리고 단순성은 항상 더 나은 유지 관리의 핵심입니다.

우리는 Data Mapper을 선택할 것입니다!

NestJS + TypeORM 개발 환경에서 Repository를 사용하는 모듈을 쓸 수 있기 때문입니다.

Repository를 사용하면 어디서든지 접근 가능합니다.

실제로 구현하는 서비스에서 접근이 가능하고 테스팅할 때도 접근이 가능하기 때문에 Data Mapper을 골랐습니다.

Injecting The Repository

Repository를 사용하기 위해 다음과 같이 합니다.

restaurants.module.ts

TypeOrmModule.forFeature()을 통해 TypeOrmModule가 특정 feature을 import할 수 있게 해줍니다.

우리의 경우에 feature은 Restaurant entity입니다

providers에 RestaurantService 주입 => RestaurantResolver에서 사용 가능하게 만들어 줍니다.

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Restaurant } from './entities/restaurant.entity';
import { RestaurantService } from './restaurant.service';
import { RestaurantResolver } from './restaurants.resolver';

@Module({
  // typeorm을 이용해서 Restaurant repository를 가져왔다
  imports: [TypeOrmModule.forFeature([Restaurant])],
  providers: [RestaurantResolver, RestaurantService],
})
export class RestaurantsModule {}

restaurant.service.ts

@Inject() 데코레이터를 사용하여 Repository<Restaurant>를 RestaurantService에 삽입할 수 있습니다.

@InjectRepository()안에 entity의 이름을 넣어서 호출하면 됩니다.

restaurants는 Restaurant entity 의 repository입니다.

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Restaurant } from './entities/restaurant.entity';

@Injectable()
export class RestaurantService {
  constructor(
    @InjectRepository(Restaurant)
    private readonly restaurants: Repository<Restaurant>,
  ) {}
  getAll(): Promise<Restaurant[]> {
    return this.restaurants.find();
  }
}

restaurants.resolver.ts

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateRestaurantDto } from './dtos/create-restaurant.dto';
import { Restaurant } from './entities/restaurant.entity';
import { RestaurantService } from './restaurant.service';

@Resolver((of) => Restaurant) //Restaurant의 resolver이라는 것
export class RestaurantResolver {
  constructor(private readonly restaurantService: RestaurantService) {}
  @Query((returns) => [Restaurant])
  restaurants(): Promise<Restaurant[]> {
    return this.restaurantService.getAll();
  }
  @Mutation((returns) => Boolean)
  createRestaurant(@Args() createRestaurantDto: CreateRestaurantDto): boolean {
    console.log(createRestaurantDto);
    return true;
  }
}

Create Restaurant

restaurant.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateRestaurantDto } from './dtos/create-restaurant.dto';
import { Restaurant } from './entities/restaurant.entity';

@Injectable()
export class RestaurantService {
  constructor(
    @InjectRepository(Restaurant)
    private readonly restaurants: Repository<Restaurant>,
  ) {}
  getAll(): Promise<Restaurant[]> {
    return this.restaurants.find();
  }

  createRestaurant(
    createRestaurantDto: CreateRestaurantDto,
  ): Promise<Restaurant> {
    const newRestaurant = this.restaurants.create(createRestaurantDto);
    return this.restaurants.save(newRestaurant);
  }
}

Repository의 createa메서드를 통해 새로운 Restaurant을 만들 수 있다.

db에 저장하기 위해 save메서드를 사용해야 합니다.

restaurants.resolver.ts

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateRestaurantDto } from './dtos/create-restaurant.dto';
import { Restaurant } from './entities/restaurant.entity';
import { RestaurantService } from './restaurant.service';

@Resolver((of) => Restaurant) //Restaurant의 resolver이라는 것
export class RestaurantResolver {
  constructor(private readonly restaurantService: RestaurantService) {}

  @Query((returns) => [Restaurant])
  restaurants(): Promise<Restaurant[]> {
    return this.restaurantService.getAll();
  }

  @Mutation((returns) => Boolean)
  async createRestaurant(
    @Args('input') createRestaurantDto: CreateRestaurantDto,
  ): Promise<boolean> {
    try {
      this.restaurantService.createRestaurant(createRestaurantDto);
      return true;
    } catch (error) {
      console.log(error);
      return false;
    }
  }
}

@Args('input') createRestaurantDto: CreateRestaurantDto

  • @InputType로 DTO를 정의 했을 때 해당 DTO를 사용하는 Resolver 에서는 @Args("input") 해당 객체에 대한 이름을 반드시 넣어주어야 합니다.
  • 해당 Resolver 는 @Args("input") 명시해준 이름을 통해 하나의 객체가 들어오게 됩니다.

async를 쓰면 리턴값은 무조건 Promise입니다.

create-restaurant.dto.ts

dto, entity, graphql 셋 모두 한번에 타입을 지정하기위해 OmitType을 썼습니다.

OmitType으로 Restaurant에 지정된 타입중 id타입 빼고 나머지 타입을 줄 수 있습니다.

import { InputType, OmitType } from '@nestjs/graphql';
import { Restaurant } from '../entities/restaurant.entity';

@InputType()
export class CreateRestaurantDto extends OmitType(Restaurant, ['id']) {}

Restaurant은 ObjectType인데 dto는 InputType이다 상속을 주는 쪽이 parents일때 둘의 타입을 같게 해줘야하는데 우리의 상황이 그렇습니다. 어떻게 해야 inputType으로 바꿀 수 있는지 알아봅시다.

  • 첫번째 방법
    • OmitType의 3번째 argument로 inputType을 주면 됩니다.
import { InputType, OmitType } from '@nestjs/graphql';
import { Restaurant } from '../entities/restaurant.entity';

@InputType()
export class CreateRestaurantDto extends OmitType(Restaurant, ['id'], inputType) {}
  • 두번째 방법 restaurant.entity.ts @InputType({ isAbstract: true })을 적용해 주면 됩니다. 이 InputType이 스키마에 포함되지 않길 원한다는 뜻입니다.
import { Field, InputType, ObjectType } from '@nestjs/graphql';
import { IsBoolean, IsString, Length } from 'class-validator';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
    
//graphQL에서 사용하는 스키마를 자동으로 생성해주고 db에도 자동으로 즉시 반영해준다!
//@InputType({ isAbstract: true })통해 dto까지 가능,, 대단
@InputType({ isAbstract: true })
@ObjectType() //Restaurant을 위한 Object type을 만들어 준다.
@Entity() // typeorm이 db에 저장해준다
export class Restaurant {
  @PrimaryGeneratedColumn()
  @Field((type) => Number)
  id: number;
    
  @Field((type) => String) //@Field의 첫번째 argument로는 returnTypeFunction이 와야한다.
  @Column() //typeorm을 위한 것
  @IsString()
  @Length(5)
  name: string;
    
  @Field((type) => Boolean, { nullable: true })
  @Column()
  @IsBoolean()
  isVegan?: boolean;
    
  @Field((type) => String)
  @Column()
  @IsString()
  address: string;
    
  @Field((type) => String)
  @Column()
  @IsString()
  ownerName: string;
    
  @Field((type) => String)
  @Column()
  @IsString()
  catagoryName: string;
}

Optional Types and Columns

graphql, database, dto에 default값을 어떻게 줄 수 있을까요?? 다음과 같이 하면 됩니다.

import { Field, InputType, ObjectType } from '@nestjs/graphql';
import { IsBoolean, IsOptional, IsString, Length } from 'class-validator';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';

//graphQL에서 사용하는 스키마를 자동으로 생성해주고 db에도 자동으로 즉시 반영해준다!
//@InputType({ isAbstract: true })통해 dto까지 가능,, 대단
@InputType({ isAbstract: true })
@ObjectType() //Restaurant을 위한 Object type을 만들어 준다.
@Entity() // typeorm이 db에 저장해준다
export class Restaurant {
  @PrimaryGeneratedColumn()
  @Field((type) => Number)
  id: number;

  @Field((type) => String) //@Field의 첫번째 argument로는 returnTypeFunction이 와야한다.
  @Column() //typeorm을 위한 것
  @IsString()
  @Length(5)
  name: string;

  @Field((type) => Boolean, { defaultValue: true }) //graphql을 위한것
  @Column({ default: true }) //database를 위한것
  @IsOptional() // dto를 위한것
  @IsBoolean() // dto를 위한것
  isVegan: boolean;

  @Field((type) => String)
  @Column()
  @IsString()
  address: string;
}

GraphQL에는 { defaultValue: true } 을 넣어주면 됩니다. 그러면 default값이 true로 됩니다.

Database에서는 { default: true } 을 넣어주면 됩니다.

DTO에서는 @IsOptional() 을 넣어주면 됩니다.

Update Restaurant

첫번째 방법

update-restaurant.dto.ts

PartialType() 함수는 입력 타입의 모든 속성이 선택사항으로 설정된 타입(클래스)을 반환합니다.

import { Field, InputType, PartialType } from '@nestjs/graphql';
import { CreateRestaurantDto } from './create-restaurant.dto';

//resolve, mutation에 어떤 restaurant을 수정할건지 알려주기 위해 id가 필요하다,
@InputType()
export class UpdateRestaurantInputType extends PartialType(
  CreateRestaurantDto,
) {}

restaurants.resolver.ts

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateRestaurantDto } from './dtos/create-restaurant.dto';
import { UpdateRestaurantDto } from './dtos/update-restaurant.dto';
import { Restaurant } from './entities/restaurant.entity';
import { RestaurantService } from './restaurant.service';

@Resolver((of) => Restaurant) //Restaurant의 resolver이라는 것
export class RestaurantResolver {
  constructor(private readonly restaurantService: RestaurantService) {}

	//...Query

  @Mutation((returns) => Boolean)
  async updateRestaurant(
    @Args('id') id: number,
    @Args('input') data: UpdateRestaurantDto,
  ) {
    return true;
  }
}

이런식으로 argument에 2개를 줄 수 있는 방법이 있습니다. 하지만 argument가 많아지는걸 싫어하면 적절한 선택은 아니죠.

두번째 방법

update-restaurant.dto.ts

이와같이 새로운 @InputType()을 만들어 하나로 합쳐줄 수 있습니다.

import { Field, InputType, PartialType } from '@nestjs/graphql';
import { CreateRestaurantDto } from './create-restaurant.dto';

//resolve, mutation에 어떤 restaurant을 수정할건지 알려주기 위해 id가 필요하다,
@InputType()
class UpdateRestaurantInputType extends PartialType(
  CreateRestaurantDto,
) {}

@InputType()
export class UpdateRestaurantDto {
  @Field((type) => Number)
  id: number;

  @Field((type) => UpdateRestaurantInputType)
  data: UpdateRestaurantInputType;
}

restaurants.resolver.ts

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateRestaurantDto } from './dtos/create-restaurant.dto';
import { UpdateRestaurantDto } from './dtos/update-restaurant.dto';
import { Restaurant } from './entities/restaurant.entity';
import { RestaurantService } from './restaurant.service';

@Resolver((of) => Restaurant) //Restaurant의 resolver이라는 것
export class RestaurantResolver {
  constructor(private readonly restaurantService: RestaurantService) {}

  //...Query

  @Mutation((returns) => Boolean)
  async updateRestaurant(
    @Args('input') updateRestaurantDto: UpdateRestaurantDto,
  ) {
    return true;
  }
}

더욱 깔끔해 졌죠?? 저는 이 방법이 더 좋은 것 같아요.

restaurant.service.ts

import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { CreateRestaurantDto } from './dtos/create-restaurant.dto';
import { UpdateRestaurantDto } from './dtos/update-restaurant.dto';
import { Restaurant } from './entities/restaurant.entity';

@Injectable()
export class RestaurantService {
  constructor(
    @InjectRepository(Restaurant)
    private readonly restaurants: Repository<Restaurant>,
  ) {}

  ...

  updateRestaurant({ id, data }: UpdateRestaurantDto) {
    // update()는 db에 해당 entity가 있는지 확인하지 않는다.
    // db에 entity가 있는지 없는지 확인하지 않고 update query를 실행한다.
    return this.restaurants.update(id, { ...data });
  }
}

restaurants.resolver.ts

import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateRestaurantDto } from './dtos/create-restaurant.dto';
import { UpdateRestaurantDto } from './dtos/update-restaurant.dto';
import { Restaurant } from './entities/restaurant.entity';
import { RestaurantService } from './restaurant.service';

@Resolver((of) => Restaurant) //Restaurant의 resolver이라는 것
export class RestaurantResolver {
  constructor(private readonly restaurantService: RestaurantService) {}

  //...Query

  @Mutation((returns) => Boolean)
  async updateRestaurant(
    @Args('input') updateRestaurantDto: UpdateRestaurantDto,
  ): Promise<boolean> {
    try {
      await this.restaurantService.updateRestaurant(updateRestaurantDto);
      return true;
    } catch (error) {
      console.log(error);
      return false;
    }
  }
}

0개의 댓글