[NestJS] - 10. TypeORM Insert / Update

2023. 2. 23. 03:34
반응형

이제 앞서 만든 REST API를 데이터베이스와 연동해보도록 하자. 우선 1차 목표는 다른 테이블과의 연관에 상관없이 TypeORM을 이용해서 Product에 데이터를 삽입하고, 업데이트 해보는 과정을 해보겠다.

 


Controller

 

▶ products.controller.ts

@Controller('product')
export class ProductsController {
  constructor(private readonly productService: ProductsService) {}

  @Post()
  createProduct(@Body() input: CreateProductInput): Promise<Product> {
    return this.productService.create(input);
  }

  @Put(':id')
  updateProduct(
    @Param('id') id: number,
    @Body() input: UpdateProductInput,
  ): Promise<Product> {
    if (id != input.id) {
      throw new ForbiddenException();
    }
    return this.productService.modify(input);
  }

}
  • POST : /  -> DTO를 Input으로 받아 데이터베이스에 Insert한 후 삽입한 객체를 반환
  • PUT : /:id -> DTO를 input으로 받아 데이터베이스에 Update한 후 삽입한 객체를 반환

지금까지 개발자들은 모범적인 RESTful한 API 설계를 위해서 많은 개발자들이 논의를 해왔다. 물론 가장 우선순위는 내부 API 규칙을 따르는 것이지만 그렇지 않았을 때, Create나 Update의 경우 결과 객체를 반환해 주는 것을 권하는 편이었다. 왜냐하면 클라이언트 입장에서는 새로운 데이터를 생성하거나 변경한 후 갱신이 필요할 때, 또다시 Read 요청을 하지 않고 Response의 값을 사용하면 되기 때문이다.

 

▶ updateProduct.dto.ts

export class UpdateProductInput {
  @IsNumber()
  id: number;

  @IsString()
  @IsOptional()
  name?: string;

  @IsString()
  @IsOptional()
  description?: string;

  @IsInt()
  @IsOptional()
  price?: number;
}

또한 PUT의 경우 업데이트할 ID를 Path Parameter과 Body 둘 중 어디에 포함시켜야 할 지에 대한 이슈도 있는데, 별도의 인증에 대한 고려할 점이 없다고 가정할 때에는, Parameter와 Body 둘 다 포함시키면서 일치여부를 판단하는 것을 권하고 있었다.

따라서 일치하지 않을 시 403을 반환하도록 하였다.

 

 

 


Service

 

▶ products.service.ts

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {}

}

Service에서 TypeORM 모듈의 기능을 사용하기 위한 Repository를 주입받는다.

 

@InjectRepository()로 사용할 Entity를 명시한 대상 Repository를 주입시켜준다. 모듈에서 forFeature에 등록한 Repository들이 대상이다.

 

 


Module

 

▶ products.module.ts

@Module({
  imports: [TypeOrmModule.forFeature([Product])],
  controllers: [ProductsController],
  providers: [ProductsService],
})
export class ProductsModule {}

위 서비스에서 Repository를 사용하기 위해 TypeORM 모듈을 Inport하고, forFeature에 사용할 대상 Entity들을 리스트 형태로 인수에 넘겨준다.

 


Insert

 

save() 사용하기

▶ products.service.ts

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {}

  async create(input: CreateProductInput): Promise<Product> {
    const result = await this.productRepository.save({
      ...input,
    });
    return result;
  }
}

 

  • Respository.save(삽입할 모델)는 TypeORM이 모델을 데이터베이스에 Insert되도록 SQL문을 실행하게 한다.
  • 반환형은 Insert된 결과 Row를 모델 클래스 형태로 반환한다.
  • 비동기 함수이므로 async/await을 사용하며, 반환형은 Promise이다.

 


Update

 

save() 사용하기

▶ products.service.ts

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {}

  async modify(input: UpdateProductInput): Promise<Product> {
  
    const product = await this.productRepository.findOne({
      where: { id: input.id },
    });
    
    const newProduct = {
      ...product, //기존 Object
      ...input, //해당 부분 갱신. (포함된 ID로 Select한 이후 Update)
    };
    /*
    실제로는 이런 형태이다.
    const newProduct = {
      id: 1,
      name: "필통",
      description: "튼튼합니다",
      price: 1000
    };
    */
    
    const result = await this.productRepository.save(newProduct);
    return result;
  }
}
  • Insert와 마찬가지로 Respository.save(업데이트할 모델)을 사용하는데 해당 메서드가 자동으로 Insert/Update를 구분해주기 때문이다.
  • save()는 인수 모델에 PK가 포함되었을 시, 2번의 쿼리를 실행한다. 첫번째는 객체의 존재여부 확인을 위한 Select, 두번째는 찾은 객체에 새로운 모델에서 변경점을 찾아 덮어쓰기 하는 것이다. 이 점이 함정일 수도 있겠다.
  • 클라이언트에서 받은 UpdateProductInput은 ID를 제외한 나머지 속성은 Optional하게 받았다. 따라서 newProduct에 구조분해 할당을 통해서 기존 Object와 갱신되어야 하는 Object를 합친 후 Update 시켜주었다.
  • findOne()은 기존 Object를 Select하는 기능인데 다음 장에서 다루어보겠다.
  • 비동기 함수이므로 async/await을 사용하며, 반환형은 Promise이다.

 

update() 사용하기

▶ products.service.ts

@Injectable()
export class ProductsService {
  constructor(
    @InjectRepository(Product)
    private readonly productRepository: Repository<Product>,
  ) {}

  async update(input: UpdateProductInput): Promise<Product> {
    const { id, ...newInput } = input;
    
    const result = await this.productRepository.update(
      { id: id },
      { ...newInput },
    );
    /*
    실제로는 이런 형태이다
    const result = await this.productRepository.update(
      { id: 1 },
      { 
      	description: "튼튼합니다",
      	price: 1000
      },
    );
    */
    
    return result.affected ? true : false; //행이 영향을 받았는지 확인
  }
}
  • save() 말고 update()를 통해서도 쿼리를 실행시킬 수 있다.
  • update()는 인수로 두개의 객체를 받는다. 첫번째 인수는 Where문에 속한다. 즉 일치하는 ID에 로우를 업데이트하겠다는 뜻이다.
  • 두번째 인수로는 갱신시킬 Object를 넣는데, 차이점은 모든 컬럼들을 포함하지 않아도 된다는 점이다. 해당 컬럼들에 해당하는 부분들만 Update가 이루어져 불필요한 작업을 하지 않는다.
  • 특이점은 save()와 달리 성공실패 여부 Object를 반환하며, 결과 모델을 반환하지 않는다는 점이다. 결과 모델을 반환하지 않으면 위에서 말했던 것처럼 클라이언트 측에서 갱신 후 재요청이 필요하거나, 응답을 주기 전 다시 Select를 해주어야 한다.

 


결과

Postman으로 요청 시 Insert 및 Update가 잘 이루어지는 것을 확인할 수 있다.

다음 포스팅에서는 TypeORM의 Select와 Delete에 대해서 다루어보겠다.

 

 

 

 


해당 강의를 들으면서 학습한 내용을 바탕으로 저만의 프로젝트를 만드는 과정을 기록하여 남기는 것을 목표로 하고 있습니다.

주관적인 생각이 들어가 있을 수 있으므로 혹시 틀린 내용이 있다면 피드백 부탁드립니다.

https://www.inflearn.com/course/%EB%B6%80%ED%8A%B8%EC%BA%A0%ED%94%84-%EB%B0%B1%EC%97%94%EB%93%9C-%EA%B3%A0%EB%86%8D%EC%B6%95-%EC%BD%94%EC%8A%A4/dashboard

 

[인프런x코드캠프] 부트캠프에서 만든 고농축 백엔드 코스 - 인프런 | 강의

코딩과 사랑에 빠져버린 8년차 풀스택 개발자 Captain의 사심이 가득 담긴 커리큘럼이에요. 백엔드의 모든 것을 다 알려주고 싶은 Captain의 마음이 녹아있죠! 이 강의를 듣다보면 '이렇게까지 알려

www.inflearn.com

 

반응형

BELATED ARTICLES

more