1. 기본 설정및 cli이용방법
다른 여타 라이브러리 처럼, Nestjs도 매우 친절한 cli 명령어들을 제공한다.
- 새로운 프로젝트 생성 nest new <name> [options]
- controller / service / module 생성
- nest generate <schematic> <name> 위의 schematic에 controller, service, module 등을 넣으면 된다. 추가 목록은 아래 링크를 참고하자. https://docs.nestjs.com/cli/usages
Controller로 라우팅하기
nest는 module을 중심으로 controller, service 이렇게 3가지 component로 module을 구성한다.
Client쪽에서 request를 보내게 되면, Controller에 먼저 도착하여 routing이 되게 된다. 아래 예를 보며 다시 정리해보자.
import { Body, Controller, Get, Post } from '@nestjs/common';
import { MenusService } from './menus.service';
import { Lunch } from '../entities/lunchLists.entity';
//1. Controller('menus') 데코레이터를 만나게 되면,
//[base_api]/menus 로 오는 요청은 이곳에 다걸리게 된다.
@Controller('menus')
export class MenusController {
constructor(private readonly MenusService: MenusService) {}
//2. [base_api]/menus 로들어 오는 Get요청을 받을 시,
//saySth이라는 메소드가 실행되게 되고, 이때 saySth은 Promise객체인, Lunch entity의 구조를 가지고,
//[]형태를 띈 값을 리턴하게 된다.
@Get()
saySth(): Promise<Lunch[]> {
//saySth은 Service에 정의한 findAll이라는 메소드를 실행시키고 그 값을 리턴한다.
return this.MenusService.findAll();
}
//3. /[base_api]/menus Post요청을 받을 경ㅇ,
// addMenu라는 메소드가 실행되게 된다.
// 이때 request.body 값으로, menu라는 값을 받게 되고, 메뉴는 name, category, etc...타입을 받게 된다는 뜻이다.
@Post()
addMenu(
@Body()
menu: {
name: string;
category: string;
menu: string;
isNoodle: string;
distance: string;
},
) {
//addMenu는 Service에 정의한 addMenu이라는 메소드를 실행시키고 그 값을 리턴한다.
return this.MenusService.addMenu(menu);
}
}
위의 코드에서 알 수 있듯이, Controller에서 처리하기 복잡한 비지니스 로직을 정의하는 곳이 바로 Service 컴포넌트다.
Service: 복잡한 비지니스 로직을 정의
일반적으로 Service컴포넌트에서는 controller 에서 정의하기 복잡한 비지니스 로직을 정의하는 곳이다. 바로 코드로 알아보자.
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Lunch } from '../entities/lunchLists.entity';
import { Repository } from 'typeorm';
@Injectable()
export class MenusService {
//contructor을 통해서, 해당 service에서 사용할 디비인 Lunch를 들고 오는 모습이다.
//이를 통해, this.lunchRepository 와 같은 방식으로 DB에 접근 할 수 있다.
//아래에 보이는 것처럼, DB값을 불러온다던가, 추가하는 로직을 이곳에서 생성하고 Controller에서 사용할 수 있다.
constructor(
@InjectRepository(Lunch) private lunchRepository: Repository<Lunch>,
) {}
findAll(): Promise<Lunch[]> {
return this.lunchRepository.find();
}
addMenu(m: {
name: string;
category: string;
menu: string;
distance: string;
isNoodle: string;
}) {
const newMenu = {
...m,
lastVisit: Date(),
count: 1,
};
this.lunchRepository.insert(newMenu);
return newMenu;
}
}
Module: Service, Controller를 하나의 module로 묶어주는 역할
위에서 작성한 menus.controller.ts, menus.service.ts, DB인 Lunch를 등록해줄 곳이 필요하다. 그 등록(종속성 주입)을 하는 곳이 바로 Module이다.
이 역시 바로 코드로 알아보자.
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MenusController } from './menus.controller';
import { Lunch } from '../entities/lunchLists.entity';
import { MenusService } from './menus.service';
@Module({
//TypeOrm을 통해, 해당 module에서 사용할 Lunch DB를 등록하는 모습이다.
imports: [TypeOrmModule.forFeature([Lunch])],
//provider에는 사용할 서비스를 넣는다.
providers: [MenusService],
//controllers 에는 사용할 컨트롤러를 넣는다.
controllers: [MenusController],
exports: [MenusService],
})
//이렇게 만든 module은 언제든지 다른 모듈(e.g. App.module.ts)에서 주입 할 수 있다.
export class MenusModule {}
TypeOrm을 이용한 migration / DB(mysql)연동
자, 이제 위에서 만든 코드들을 어떻게 DB와 연동시킬 지 알아보자. (여기서부터는 내용들이 너무 많은 시도를 하다보니 어떻게 작동 되었는지...확실치 않다. 추후 업데이트예정)
- entity를 만들자. entity??? ⇒ entity는 typeScript를 사용할 때 타입선언을 편리하게 해줄 뿐 아니라, DB 테이블의 뼈대역할을 한다.
//src/entities/lunchList.entity.ts
import { Entity, Column, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
export class Lunch {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column()
category: string;
@Column()
menu: string;
@Column()
distance: string;
@Column()
isNoodle: string;
@Column()
lastVisit: string;
@Column()
count: number;
}
// src/entities/lunchList.provider.ts
import { Connection } from 'typeorm';
import { Lunch } from './lunchLists.entity';
export const lunchListProviders = [
{
provide: 'LUNCH_REPOSITORY',
useFactory: (connection: Connection) => connection.getRepository(Lunch),
inject: ['DATABASE_CONNECTION'],
},
];
- migrantion 파일 만들기
//src/migration/migration.js
module.exports = {
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '!dlgudxo90',
database: 'lunch',
entities: ['entity/*.js'],
migrationsTableName: 'custom_migration_table',
migrations: ['migration/*.js'],
cli: {
migrationsDir: 'migration',
},
};
- app을 통해 연결하기
//app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { MenusController } from './menus/menus.controller';
import { MenusModule } from './menus/menus.module';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Lunch } from './entities/lunchLists.entity';
@Module({
imports: [
MenusModule,
//여기에서 내가 접근할 db에 대한 config를 정한다.
TypeOrmModule.forRoot({
type: 'mysql',
host: 'localhost',
port: 3306,
username: 'root',
password: '!dlgudxo90',
database: 'lunch',
entities: [Lunch], // 사용할 entity의 클래스명을 넣어둔다.
synchronize: true, // prod.일때에는 false로 해두는 게 안전하다.
autoLoadEntities: true,
}),
],
controllers: [AppController, MenusController],
providers: [AppService],
})
export class AppModule {}
- cli를 통해 migration 파일 만들기 typeorm migration:create -n [migrationfile__name]
- npm start 명령을 통해 서버실행 ⇒처음에는 이 과정이 없어도, 바로 DB에 Table이 생성될 줄 알았으나, npm start를 함으로써 dist(build파일인듯하다)폴더가 생기고, 이를 기반으로 DB migration이 일어남으로 꼭 필요한 과정이다.
**주의사항. 처음에 entity를 만들고, column하나를 추가하고 싶어, entity파일에 추가한 이후, 위의 과정을 진행 했으나, 제대로 적용이 안되는 것을 볼 수 있었다. 이는 dist폴더에 있는 entity파일을 기반으로 DB 테이블 생성, migration이 진행되기 때문 인듯 하다. ⇒ 즉, dist폴더 자체를 지운뒤, build를 다시 하면 정상 작동이 되는 것을 볼 수 있었다.
참고로 dist 폴더는 build폴더인 듯한데, npm start나, npm run build를 하게 되면 생기게 되는것을 볼 수 있었다.
TypeOrm을 이용하여, Table 생성하기
- databaseModule.ts파일 만들기
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
// import { join } from 'path';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule.forRoot()],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
host: configService.get('DATABASE_HOST'),
port: configService.get('DATABASE_PORT'),
username: configService.get('DATABASE_USER'),
password: configService.get('DATABASE_PASSWORD'),
database: configService.get('DATABASE_NAME'),
//DB는 직접 생성을 해야하며, 내가 만들어 놓은 entity를 기반으로하여
//Table을 생성하고 싶다면, 아래의 entities에 경로를 넣어 서버실행을 하면 자동으로 생성된다.
//단, 여기서, **['dist/**/*.entity.js'] 이라고 되어 있지만,
//entities: [__dirname + '/../**/*.entity{.ts,.js}'],
//으로도 가능해야 하지만... 현재 경우에 2번째는 작동하지 않았다.
//synchronize 옵션을 true로 설정 해줘야 table이 생성되는걸 확인 할 수 있었다.
//하지만, 이는 dev에서만 사용해야 하며, prod.에서는 이 모드를 사용하지 않고,
//migration을 이용하여 생성해야 한다고 한다.**
**entities: ['dist/**/*.entity.js'],**
**migrations: ['dist/migration/**/*.js'],**
**synchronize**: **true**,
autoLoadEntities: true,
cli: {
migrationsDir: 'src/migration',
},
timezone: 'Z',
// logging: ['query', 'error'],
logging: ['error'],
}),
}),
],
})
export class DatabaseModule {}
- app.Module에 database.module등록하기
import { Module } from '@nestjs/common';
import { DatabaseModule } from './database/database.module';
...
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: process.env.NODE_ENV === 'dev' ? '.env.dev' : '.env.prod',
validationSchema: Joi.object({
DATABASE_HOST: Joi.string().required(),
DATABASE_PORT: Joi.number().required(),
DATABASE_USER: Joi.string().required(),
DATABASE_PASSWORD: Joi.string().required(),
DATABASE_NAME: Joi.string().required(),
JWT_ACCESS_TOKEN_SECRET: Joi.string().required(),
JWT_ACCESS_TOKEN_EXPIRATION_TIME: Joi.string().required(),
JWT_REFRESH_TOKEN_SECRET: Joi.string().required(),
JWT_REFRESH_TOKEN_EXPIRATION_TIME: Joi.string().required(),
}),
}),
UsersModule,
RolesModule,
AuthModule,
//이렇게 등록을 해주어야 적용이 된다.
**DatabaseModule,**
ElasticsearchModule,
BsmSearchModule,
MbrSearchModule,
CertSearchModule,
MrLogModule,
TmLogModule,
DetectPolicyModule,
],
controllers: [AppController],
providers: [AppService, { provide: APP_GUARD, useClass: JwtAuthGuard }],
})
export class AppModule {}
Docker-compose를 이용하여, DB에 연결하기(docekrDB)
이 방법으로 DB 와 연결하게 되면, docker를 내리게 되면 DB도 삭제되게 된다.
1. docker-compose.yml 파일 작성
version: "3.6"
services:
backend:
container_name: mcBack
build:
context: ./backend
dockerfile: Dockerfile
ports:
- 9091:9091
volumes:
- "./backend:/app"
- "/app/node_modules"
environment:
TZ: "Asia/Seoul"
frontend:
container_name: mcFront
build:
context: ./frontend
dockerfile: Dockerfile
ports:
- 9090:9090
volumes:
- "./frontend:/app"
- "/app/node_modules"
environment:
TZ: "Asia/Seoul"
//여기에서 중요한 것은 local로 연결 하고 싶다면, database부분을 아래처럼 따로 설정을 해줘야 한다는 것이다.
database: # 서비스 명
image: mysql # 사용할 이미지
container_name: gmbdmc # 컨테이너 이름 설정
restart: always
ports:
- "3306:3306" # 접근 포트 설정 (컨테이너 외부:컨테이너 내부) <- 컨테이너 내부는 무조건 3306
environment: # -e 옵션
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: gmbd
MYSQL_USER: username
MYSQL_PASSWORD: password
2. database.module.ts에서 yml파일에 만든 설정과 동일하게 작성
**** 주의 ****
위에 적은 database(#서비스명)을 host명으로 작성해줘야 한다
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { join } from 'path';
@Module({
imports: [
TypeOrmModule.forRootAsync({
imports: [ConfigModule.forRoot()],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
type: 'mysql',
//중요한 것은 host명을 위에 적어둔 yml파일의 database와 동일하게 작성해줘야 한다는 것이다.
host: 'database',
port: 3306,
username: configService.get('DATABASE_USER'),
password: configService.get('DATABASE_PASSWORD'),
database: configService.get('DATABASE_NAME'),
entities: [join(__dirname, '/../**/*.entity.js')],
migrations: ['dist/migration/**/*.js'],
synchronize: true,
autoLoadEntities: true,
cli: {
migrationsDir: 'src/migration',
},
timezone: 'Z',
// logging: ['query', 'error'],
logging: ['error'],
}),
}),
],
})
export class DatabaseModule {}
...
import { DatabaseModule } from './database/database.module';
...
@Module({
imports: [
...
DatabaseModule,
...
],
controllers: [AppController],
providers: [AppService, { provide: APP_GUARD, useClass: JwtAuthGuard }],
})
export class AppModule {}
3. app.module에 database.module 등록
TypeOrm에 기반한 migration 설정 및 실행
***** TS이용자는 npm install -g ts-node 설치가 필요하다 *****
1. ormconfig.json 환경설정
server/ormconfig.json과 같이 root 폴더에 작성한다.
[
{
"name": "default",
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "root",
"password": "!dlgudxo90",
"database": "lunch",
"entities": [
"dist/**/*.entity{ .ts,.js}"
],
"synchronize": true,
"migrations": [
"dist/migrations/*{.ts,.js}"
],
"migrationsTableName": "migrations_history",
"migrationsRun": true,
"ssl": true,
"extra": {
"ssl": {
"rejectUnauthorized": false
}
}
}
]
2. app.module에서도 migrations 설정한다.
...
migrations: ['dist/migrations/*{.ts,.js}'],
migrationsTableName: 'migrations_history',
migrationsRun: true,
synchronize: true, // prod.일때에는 false로 해두는 게 안전하다.
autoLoadEntities: true,
cli: {
migrationsDir: 'migration',
},
//indicates that the CLI must create new migrations in the "migration" directory.
}),
...
3. migration file 생성
1. typeorm migration:create
typeorm migration:create -n [생성할 migration이름] -d src/migrations
(-d 옵션은 src/migrations라는 폴더를 만들고 이곳에 migration파일을 생성한다.) ****(-c 옵션은 ormconfig.js의 name과 일치하는 옵션으로 설정한다.)
2. typeorm migration:generate
npm run typeorm migration:generate -- -c production -n NewMigration
generate이란 명령으로 위와 같이 생성도 가능하지만, create와의 차이점은 entity의 모델과 DB를 비교하여, 업데이트하는 부분을 알아서 작성한다는 것이다.
하지만, 컬럼의 길이를 변경하거나 타입을 바뀔 때, 해당 테이블의 컬럼을 DROP 한 뒤, 재생성이 이뤄지기에 데이터가 날아가버린다.
참고링크: https://github.com/typeorm/typeorm/issues/3357
4. migration file 작성
다음과 같이 실행하고자 하는 query문을 migration file에 작성한다. async up 부분은 migration이 실행될 때 작동되고, async down 부분은 migration:revert 즉 취소될 때 실행되게 된다.
1. table 생성
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
export class lunch1640067060359 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
await queryRunner.createTable(
new Table({
name: 'article',
columns: [
{
name: 'id',
type: 'int4',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'title',
type: 'varchar',
isNullable: false,
},
{
name: 'content',
type: 'varchar',
isNullable: false,
},
],
}),
false,
);
}
public async down(queryRunner: QueryRunner): Promise<any> {
queryRunner.query(`DROP TABLE article`);
}
}
2. table에 default 값 insert
[참고링크]
https://github.com/nestjs/nest/issues/4539
https://orkhan.gitbook.io/typeorm/docs/delete-query-builder
import { User } from '../entities/mcUser.entity';
import { getConnection, MigrationInterface, QueryRunner } from 'typeorm';
export class defaultuserdata1640148812603 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<any> {
const userRepo = queryRunner.connection.getRepository(User);
await userRepo.insert([
{
id: 'admin',
name: '어드민',
email: 'admin@admin.com',
password:
'$2b$10$GqjQv0ZMRbDiV.2DYUjh0eFN4NKnv7d3vQqSqPj.5fvZOjbFLKYUm',
is_active: 1,
is_reset_pwd: 0,
last_login_dt: null,
// role_id: 2,
},
{
id: 'gmbda',
name: 'GMBDAdmin',
email: 'gmbda@gmbda.com',
password:
'$2b$10$kxwYTrxX5bkblQWQ1Akz7eMCGgpeeRVTaAqCPl1oF3L5YIytzYBy6',
is_active: 1,
is_reset_pwd: 0,
last_login_dt: null,
// role_id: 1,
},
{
id: 'monitor',
name: '모니터',
email: 'mo@mo.com',
password:
'$2b$10$zAqmK2WWLUc83T7b0aFZGuHEay6ILWZ3hQOoIMCc1zz/RS.QqW26W',
is_active: 1,
is_reset_pwd: 0,
last_login_dt: null,
// role_id: 3,
},
]);
}
//migration을 취소하는 down부분은 수정이 필요
public async down(queryRunner: QueryRunner): Promise<any> {
await getConnection()
.createQueryBuilder()
.delete()
.from(User)
.where('id = :id', { id: 'admin' });
await getConnection()
.createQueryBuilder()
.delete()
.from(User)
.where('id = :id', { id: 'gmbda' });
await getConnection()
.createQueryBuilder()
.delete()
.from(User)
.where('id = :id', { id: 'monitor' });
}
}
5. migration 실행(up)
**typeorm migration:run ⇒ 오류발생**
명령어를 통해, migration이 실행되었어야 하지만, 나의 경우, 계속 아래와 같은 오류가 발생하였다.
migrations are already loaded in the database.
migrations were found in the source code.
migrations are new migrations that needs to be executed.
그래서 찾아보니, typeorm이 아닌, npx를 통해 바로 migration 하라는 글을 찾았다.
https://github.com/typeorm/typeorm/issues/8230
**npx typeorm migration:run ⇒ 정상작동**
시키는대로 위의 명령어를 실행하니 정상적으로 작동하였다.
6. migration 되돌리기(down)
**typeorm migration:revert**
migration을 되돌리기 위해서, 생성된 migration table(db에 생성)에서 마지막 파일의 down을 실행하여, migration을 되돌리게 된다.
'NestJS' 카테고리의 다른 글
main.ts 에서 nestjs module 사용하기 (feat. nestjs 서버 시작 로그 남기기) (0) | 2022.05.12 |
---|---|
nestjs Logger Middleware 만들기(feat. log db저장) (0) | 2022.05.12 |
nestJS app 밖에서 nestJS application instance 사용하기 (0) | 2022.04.29 |
migration 으로 DB 초기값 설정(with typeorm) (0) | 2022.04.28 |
[NestJS] filter안에 dependency injection하기(i.e. inject service) (0) | 2022.04.15 |