본문 바로가기

NestJS - CRUD / TypeORM 본문

개발/nest.js

NestJS - CRUD / TypeORM

자전하는명왕성 2023. 2. 7. 00:00

미래의 나에게

검증을 하기 위한 두 라이브러리

yarn add class-validator 
yarn add class-transformer

 

오늘은 오늘 과제였던 CRU API 구현 / TypeORM 에 대해 리뷰한다.

 

DTO / interface

먼저 본격적인 API 구현에 앞서, 이전과 같이 dto 와 Interface 를 작성한다.

( create 를 위한 dto / update 를 위한 dto)

워낙 불려다니는 곳이 많은 친구들이라 먼저 작성해두는 것이 마음 편하다.

// dto/create-product.input.ts
 
import { Field, InputType, Int } from '@nestjs/graphql';
import { Min } from 'class-validator';

@InputType() // 입력을 할 때 필요하기에 InputType
export class CreateProductInput {
  @Field(() => String)
  product_name: string;

  @Min(0)
  @Field(() => Int)
  product_price: number;

  @Min(0)
  @Field(() => Int)
  product_weight: number;

  @Min(0)
  @Field(() => Int)
  product_kcal: number;

  @Min(0)
  @Field(() => Int)
  product_protein: number;

  @Min(0)
  @Field(() => Int)
  product_fat: number;

  @Min(0)
  @Field(() => Int)
  product_mg: number;

  @Min(0)
  @Field(() => Int)
  product_sugar: number;
}

// Field 값에는 Boolean / Float(소수점) / Date 가 들어갈 수 있다.

해당 dto 는 이전 entity에 작성한 column 값을 따르며

@ 데코레이터를 받는 Min 은 검증을 하기 위한 요소다. ( 최솟값이 0 을 의미한다.)

검증을 하기 위한 라이브러리는 위에 작성해 두었다.

// dto/update-product.input.ts

import { InputType, PartialType } from '@nestjs/graphql';
import { CreateProductInput } from './create-product.input';

@InputType()
export class UpdateProductInput extends PartialType(CreateProductInput) {}

update 를 위한 dto 는 create 를 위해 작성한 dto 를 재활용한다.

유틸리티인 PartialType 을 사용해 빈 값이라도 적용될 수 있도록 한다.

** interface 역시 create dto / update dto 를 활용하여 작성한다.

 

API

API 를 위한 초기 설정

// module.ts
@Module({
  imports: [TypeOrmModule.forFeature([Product])], // entity를 전달하기 위한 메서드
  				// import 하지 않을 시, DB 연결이 되지 않는다
  providers: [
    ProductsResolver, // DI
    ProductsService,
  ],
})
export class ProductsModule {}


// service.ts 

@Injectable() // DI
export class ProductsService {
  constructor(
    @InjectRepository(Product) // DB에 저장하기 위해 entity 'Product'에 레파지토리 주입
    private readonly productsRepository: Repository<Product>,
  ) {}

 

Create API 

  // create
  // resolver.ts
  
  @Mutation(() => Product)
  createProduct(
    @Args('createProductInput') createProductInput: CreateProductInput, 
    // dto 를 가져와, createProductInput 에 만들 객체 타입을 설정해 줌
  ): Promise<Product> { // 이때 DB에 저장후 return 하는 과정이 있음으로 Promise 사용
  						// Product 는 '엔티티'
    return this.productsService.create({ createProductInput }); 
    					// service 에서 반환된 값을 module로 반환
  }


  // service.ts
  create({ createProductInput }: IProductsServiceCreate): Promise<Product> { 
  					// 인터페이스 & 위와 같음
    const result = this.productsRepository.save({ 
      ...createProductInput, // // result 를 DB에 저장
    });
    return result;
  }

 

Read API

// resolver.ts

// findAll
  @Query(() => [Product])	// 여러 개이므로 대괄호로 표현
  fetchProducts(): Promise<Product[]> {
    return this.productsService.findAll(); // service 에서 받은 값 module로 반환
  }

  // findOne
  @Query(() => Product)
  fetchProduct(
    @Args('productId') productId: string, // fetch 를 하기 위한 id 값을 아규먼트로 받음
  ): Promise<Product> {
    return this.productsService.findOne({ productId }); 
    				// id 를 service로 전달 후 받은 값 module 에 반환
  }
  
  // service.ts 
  findAll(): Promise<Product[]> {
    return this.productsRepository.find(); 
    			// TypeORM find() 사용 / 해당되는 전체값 fetchProducts 에 전달
  }

  // findOne
  findOne({ productId }: IProductServiceFindOne): Promise<Product> {
    return this.productsRepository.findOne({ where: { id: productId } });
    			// findOne 으로 조건식 'where'에 있는 값 리턴 후 fetchProduct 에 전달
  }

 

Update API

// resolver.ts
@Mutation(() => Product)
  updateProduct(
    @Args('productId') productId: string, //
    @Args('updateProductInput') updateProductInput: UpdateProductInput,
  ): Promise<Product> {
    return this.productsService.update({ productId, updateProductInput });
  }
  
// service.ts
  async update({
    productId,	// update 를 위한 id
    updateProductInput, // update 를 위한 변경된 값
  }: IProductServiceUpdate): Promise<Product> {
    const product = await this.findOne({ productId }); 
    		//  async & await / findOne 이 이뤄질 때까지 기다림
    this.checkUpdate({ product });
    const result = this.productsRepository.save({ 
      ...product,
      ...updateProductInput, // 객체의 경우, 나중 값이 기존 값을 덮어씌움
    });
    return result;
  }

  // 검증
  checkUpdate({ product }: IProductServiceCheckUpdate): void {
    // if ('검증할 조건 아직 없음') {
    //   throw new UnprocessableEntityException('판매 불가 상품입니다.');
    // }	검증할 것이 없으므로 주석 처리
    console.log(product, '검증 완료되었습니다.');
  }

 

Delete API

// resolver.ts

@Mutation(() => Boolean)
  deleteProduct(@Args('productId') productId: string): Promise<boolean> {
    return this.productsService.delete({ productId });
  }
  
// service.ts
async delete({ productId }: IProductServiceDelete): Promise<boolean> {
    const result = await this.productsRepository.softDelete({ id: productId });
    return result.affected ? true : false;	// 삭제한 데이터를 반환할 수 없기에 
    						// affected 를 통해 동작했는지를 boolean 값으로 반환
  }
}

interface IProductServiceDelete {
  productId: string;
}

// entity.ts
 @DeleteDateColumn()
  deleteAt: Date;	// 삭제 여부를 콜론으로 저장해 등록

주의 깊게 봐야 할 것은 softDelete.

softDelete 는 TypeORM 에서 제공하는 기능으로 DB 데이터의 삭제를 돕는 역할을 한다.

중요한 것은 DB에서 완전히 삭제되는 것이 아니라, '삭제된 척'을 하는 것.

이때, entity에서 softDelete와 함께 사용할 수 있는 @DeleteDateColumn 을 사용하여 삭제 시간을 기록할 수 있으며,

해당 DB 값이 null 이 아닌 경우, 보여주지 않게 된다.

 

softDelete & softRemove

  • softDelete : 여러 ID 한번에 지우기 불가능 / 다른 칼럼으로도 삭제 가능
  • softRemove : 여러 ID 한번에 지우기 가능 / 아이디로만으로도 삭제 가능

 

예외처리

// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './commons/filter/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe()); // 검증을 위한 Pipe
  app.useGlobalFilters(new HttpExceptionFilter()); // 예외 처리를 위한 Filter
  await app.listen(3000);
}
bootstrap();

// filter / http-exception.filter.ts
import {
  Catch,
  ExceptionFilter,
  HttpException,
} from '@nestjs/common';

@Catch(HttpException)	// Catch(httpException) 으로 예외처리 가능
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException) {
    const status = exception.getStatus(); // 예외 시 상태 메시지
    const message = exception.message;	// 예외 내용

    console.log('===================================');
    console.log('예외가 발생했습니다.');
    console.log('예외 내용 :', message);
    console.log('예외 코드 :', status);
    console.log('===================================');
  }
}

 

작성한 뒤 playground 에서 확인한 모습

create

 

read

 

update

 

delete

Comments