NestJS는 TypeORM과 함께 사용되어 데이터베이스를 쉽게 다룰 수 있습니다.
node.js에서 주로 사용하는 ORM은 Sequelize와 TypeORM이 있는데 NestJS에서는 TypeORM을 더 선호합니다.
이번 포스팅에서는 이 TypeORM을 사용하여 NestJS에서 Database를 다루는 방법을 알아보겠습니다.
TypeORM 이란?
TypeORM은 “Typescript + ORM”을 의미로 Typescript에서 사용하는 ORM Framework를 의미합니다.
TypeORM은 강력한 ORM(Object-Relational Mapping) 라이브러리로, Typescript를 이용하여 객체와 관계형 데이터베이스를 자동으로 맵핑 시켜주는 기술을 의미합니다.
Typescript로 작성된 Class와 Decorator를 사용하여 Entity를 정의하고 이러한 Entity를 사용하여 Database의 테이블을 관리할 수 있습니다.
또한, TypeORM은 Database의 Query를 쉽게 작성할 수 있고 Database 의 관계를 쉽게 설정하고 관리할 수 있게 도와줍니다.
설치
포스팅 에서는 MariaDB를 사용하여 Database 연동을 진행해보려고 합니다.
MariaDB 설치
MariaDB는 다운로드 링크를 통해 OS와 버전을 확인하여 다운로드 받은 설치파일로 설치를 진행하면 됩니다.
Database 생성
MySQL Client를 실행하고 $ create database devitworld COLLATE utf8_general_ci;
을 통해 Database를 생성합니다. (여기서 devitworld
는 본 포스팅에서 사용하는 데이터베이스 이름입니다.)
TypeORM 설치
Database를 연동하고자 하는 패키지에 npm을 통해 필요한 라이브러리를 설치합니다.
$ npm install --save @nestjs/typeorm typeorm mysql2 @nestjs/config
설정 및 테이블 생성
환경설정
루트 경로에 환경 설정 파일을 생성합니다. (.gitignore 에 해당 파일을 추가해서 노출시키지 않을 수 있습니다.)
/development.env
DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASS= DB_DATABASE=devitworld // database name
Module, Controller, Service 생성
이전에 배운 NestJS CLI를 통해 module, controller, service를 생성합니다.
$ nest g module users $ nest g controller users $ nest g service users
Entity 생성
User 정보를 보관할 Entity를 생성합니다.
@PrimaryGeneratedColumn
Decorator는 Primary Key와 auto increment를 자동으로 적용시켜줍니다.
TypeOrm 에서 사용되는 Decorator는 다음 문서를 참조 합니다.
/users/entities/user.entity.ts
import { Entity, Column, PrimaryGeneratedColumn, Unique } from 'typeorm'; @Entity() @Unique(['userId']) export class User { @PrimaryGeneratedColumn() key: number; @Column() userId: string; @Column() password: string; @Column() nickName: string; @Column() age: number; @Column() email: string; }
AppModule
(app.module.ts)에 연결하기 위해
다음과 같이 AppModule
(app.module.ts)에 ConfigModule
을 통해 추가한 환경 설정파일을 포함하고, 로드한 Config(process.env
)를 이용해 TypeOrmModule
을 설정합니다. entities 속성에는 위에서 추가한 User
entity class를 추가해줍니다.
import { Module } from '@nestjs/common'; // Database import { TypeOrmModule } from '@nestjs/typeorm'; import { ConfigModule } from '@nestjs/config'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { LoggerModule } from './core/logger/logger.module'; import { User } from './users/entities/user.entity'; import { UsersModule } from './users/users.module'; @Module({ imports: [ ConfigModule.forRoot({ envFilePath: ['.development.env'] }), TypeOrmModule.forRoot({ type: 'mysql', host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT), username: process.env.DB_USER, password: process.env.DB_PASS, database: process.env.DB_DATABASE, entities: [User], synchronize: true, logging: true, }), LoggerModule, UsersModule, ], controllers: [AppController], providers: [AppService], }) export class AppModule {}
서버를 실행시키면 앞서 생성한 데이터베이스에 다음과 같이 자동으로 테이블이 생성된다.
데이터 처리
이제 Controller에 Rest API를 통해 CRUD를 수행할 수 있도록 Controller에 Routing 을 구현하고 Service에 비즈니스 로직을 구현한다.
DTO(Data Transfer Object) 생성
Create 와 Update를 위한 DTO를 만들어 준다.
Class validator를 사용하여 타입의 유효성을 검증한다.
($ npm install class-validator --save
를 통해 설치할 수 있다.)
create-user.dto
import { IsEmail, IsNumber, IsOptional, IsString } from 'class-validator'; export class CreateUserDTO { @IsString() userId: string; @IsString() password: string; @IsString() nickName: string; @IsNumber() age: number; @IsOptional() @IsEmail() email: string; }
update-user.dto
nestjs/mapped-types 를 사용하면 DTO의 재사용성을 높일 수 있습니다.
($ npm i --save @nestjs/mapped-types
)
따라서 다음과 같이 PartialType
을 사용하여 CreateUserDto
에서 사용했던 모든 Column 값을 option으로 변경하여 사용할 수 있습니다.
import { PartialType } from '@nestjs/mapped-types'; import { CreateUserDto } from './create-user.dto'; export class UpdateUserDTO extends PartialType(CreateUserDto) {}
Module 연결
forFeature()
method를 사용해서 현재 모듈 범위에 어떤 저장소(Repository)를 등록할지 정의합니다.
등록한 저장소들은 @InjectRepository()
를 통해 서비스 등 다른 부분으로 Inject 될 수 있게 됩니다.
UsersModule
(/users/users.module.ts)
import { Module } from '@nestjs/common'; import { UsersController } from './users.controller'; import { UsersService } from './users.service'; // Database import { TypeOrmModule } from '@nestjs/typeorm'; import { User } from './entities/user.entity'; @Module({ imports: [TypeOrmModule.forFeature([User])], controllers: [UsersController], providers: [UsersService], }) export class UsersModule {}
Controller에서 사용
Request를 받아 Service를 통해 비즈니스를 수행하고 반환하는 Controller를 다음과 같이 정의합니다.
REST API 의 각 method들에 대해서는 다음 포스팅을 참조합니다.
Get은 Path를 이용하는 방법과 Query를 이용하는 방법을 사용했습니다.
userId를 path상의 id로 받아와서 사용합니다.
UsersController
(/users/users.controller.ts)
import { Controller, Get, Post, Patch, Delete, Param, Query, Body, } from '@nestjs/common'; import { UsersService } from './users.service'; // Entity import { User } from './entities/user.entity'; // import User entity // DTO import { CreateUserDTO } from './dtos/create-user.dto'; import { UpdateUserDTO } from './dtos/update-user.dto'; @Controller('users') export class UsersController { constructor(private readonly usersService: UsersService) {} @Get('all') async findAll(): Promise<User[]> { return this.usersService.findAll(); } @Get(':id') async findByUserId(@Param('id') id: string): Promise<User | null> { return this.usersService.findByUserId(id); } @Get() async findByQuery(@Query('id') id: string): Promise<User | null> { return this.usersService.findByUserId(id); } @Post() async create(@Body() user: CreateUserDTO) { return this.usersService.create(user); } @Delete(':id') async remove(@Param('id') id: string): Promise<void> { return this.usersService.removeByUserId(id); } @Patch(':id') async update( @Param('id') id: string, @Body() user: UpdateUserDTO, ): Promise<void> { const updatedUser: User = await this.usersService.updateByUserId(id, user); return Object.assign({ data: { ...updatedUser }, statusCode: 200, message: 'success', }); } }
Service에서 사용
Service에서 앞서 Module에서 등록한 Repository를 이용하여 추가한 DTO 등을 활용하여 Database에 접근하여 Routing된 비즈니스 로직을 수행합니다.
@InjectRepository(User)
통해 UserRepository를 inject 할 수 있습니다.
import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; // Entity import { User } from './entities/user.entity'; // import User entity // DTO import { CreateUserDTO } from './dtos/create-user.dto'; import { UpdateUserDTO } from './dtos/update-user.dto'; import { LoggerService } from '../core/logger/logger.service'; @Injectable() export class UsersService { constructor( @InjectRepository(User) private usersRepository: Repository<User>, private myLogger: LoggerService, ) {} // get async findAll(): Promise<User[]> { return await this.usersRepository.find(); } findByUserId(userId: string): Promise<User | null> { return this.usersRepository.findOne({ where: { userId: userId } }); } // create async create(user: CreateUserDTO) { await this.usersRepository.save(user); } // update async updateByUserId( userId: string, user: UpdateUserDTO, ): Promise<User | null> { const preUser = await this.findByUserId(userId); const updateUser = { ...preUser, ...user }; // overwrite preUser with user await this.usersRepository.save(updateUser); return updateUser; } // delete async removeByUserId(userId: string): Promise<void> { const targetUser = await this.findByUserId(userId); await this.usersRepository.delete(targetUser.key); } }
테스트
Postman을 통해 테스트를 진행해봅니다.
INSERT 테스트
Postman을 통해 POST method에 다음과 같이 body를 json형식으로 넣어 전송합니다.
다음과 같이 연결한 database(devitworld
)의 user table에 해당 item이 추가되었습니다.
SELECT 테스트
Postman을 통해 GET method에 path에 id를 추가하여 전송하고, 조회된 결과를 확인합니다.
UPDATE 테스트
Postman을 통해 PATCH method에 userId를 path에 주고 Body에 JSON 형식으로 업데이트 하고자하는 속성 값을 넣어 요청하고 그 결과를 응답받습니다.
DELETE
Postman을 통해 DELETE method에 userId를 path에 주어 해당 Item을 삭제합니다.