A Developing Developer

DAY 67. TypeORM 복습 본문

내일배움캠프 4기/TIL

DAY 67. TypeORM 복습

H-JJOO 2023. 2. 17. 17:10
  • TypeORM 설치

(class-vailidator 이슈 발생 시)

 

- Window

$ npm uninstall class-validator
$ npm i typeorm@0.3.0
$ npm i @nestjs/typeorm mysql
$ npm i class-validator

- Max

$ npm uninstall class-validator
$ npm i @nestjs/typeorm typeorm mysql
$ npm i class-validator
  • TypeORM 적

- TypeModul.forRoot({..}) : 모든 모듈에 적용 (데이터베이스)

- TypeOrmModule.forRootAsync : 비동기 작업(데이터베이스와 연결 설정)이 완료될 때까지 응용프로그램 시작을 지연해야하기 때문에 사용한다.

- @nestjs/config : express .env 의 역할을 수행한다. (마찬가지로 class-vailiator 이슈 발생시 삭제 후 설치 후 재설치)

$ npm i @nestjs/config

-.env

DATABASE_HOST="localhost"
DATABASE_PORT=3306
DATABASE_USERNAME="데이터베이스 아이디"
DATABASE_PASSWORD="데이터베이스 비밀번호"
DATABASE_NAME="데이터베이스 이름"

- app.module.ts (config 설정을 전역으로 참조)

import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { BoardModule } from './board/board.module';
import { TypeOrmConfigService } from './config/typeorm.config.service';

@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    TypeOrmModule.forRootAsync({
      useClass: TypeOrmConfigService,
    }),
    BoardModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

- config/typeorm.config.service.ts (.env 파일의 환경변수 읽어오는 버전)

import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config/dist';
import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm';
import { Article } from 'src/board/article.entity';

@Injectable()
export class TypeOrmConfigService implements TypeOrmOptionsFactory {
  // 생성자를 통해서 DI
  constructor(private readonly configService: ConfigService) {}
  createTypeOrmOptions(): TypeOrmModuleOptions {
    return {
      type: 'mysql',
      host: this.configService.get<string>('DATABASE_HOST'),
      port: this.configService.get<number>('DATABASE_PORT'),
      username: this.configService.get<string>('DATABASE_USERNAME'),
      password: this.configService.get<string>('DATABASE_PASSWORD'),
      database: this.configService.get<string>('DATABASE_NAME'), // 중요: board 데이터베이스는 미리 생성해놓아야 합니다!
      entities: [Article],
      synchronize: true, // Production 환경에서는 false로 설정해야 합니다.
    };
  }
}

 

  • Entity & Repository 생성 

- article.entity.js

import {
  Column,
  CreateDateColumn,
  DeleteDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';


@Entity({ schema: 'board', name: 'articles' }) // 해당 클래스가 어떤 테이블에 매핑이 되는지
export class Article {
  @PrimaryGeneratedColumn({ type: 'int', name: 'id' }) // PK
  id: number;

  @Column('varchar', { length: 10 }) // 컬럼
  author: string;

  @Column('varchar', { length: 50 })
  title: string;

  @Column('varchar', { length: 1000 })
  content: string;

  @Column('varchar', { select: false })
  password: string;

  @CreateDateColumn() // 날짜
  createdAt: Date;

  @UpdateDateColumn() 
  updatedAt: Date;

  @DeleteDateColumn() // 실제로 삭제하는 것이 아닌 Soft Delete 를 위한 삭제 날짜
  deletedAt: Date | null; // deleteAt !== NULL 삭제가 되지 않은 게시물
}

- 일반 Repository 사용은 서비스 생성자에 아래와 같이 주입하면 된다.

constructor(
  @InjectRepository(Article) private articleRepository: Repository<Article>
) {}

- board.module.ts

import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from './article.entity';
import { BoardController } from './board.controller';
import { BoardService } from './board.service';

@Module({
  // 매우 중요: 서비스에서 사용할 리포지토리 사용을 imports에 명시!
  imports: [TypeOrmModule.forFeature([Article])],
  controllers: [BoardController],
  providers: [BoardService],
})
export class BoardModule {}

! 특정 모듈 서비스에서 사용하고 싶은 레파지토리가 있다면 @Module 데코레이터의 imports 속성에 반드시 넣어줘야 DI가 원활하게 된다.

 

- board.service.ts (최종)

import _ from 'lodash';
import { Injectable } from '@nestjs/common';
import {
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common/exceptions';
import { InjectRepository } from '@nestjs/typeorm';
import { Article } from './article.entity';
import { Repository } from 'typeorm';

@Injectable()
export class BoardService {
  constructor(
    @InjectRepository(Article) private articleRepository: Repository<Article>,
  ) {}

  // 게시물 목록
  async getArticles() {
    return await this.articleRepository.find({
      where: { deletedAt: null },
      select: ['id', 'author', 'title', 'createdAt'],
    });
  }

  // 게시물 상세
  async getArticleById(id: number) {
    return await this.articleRepository.findOne({
      where: { id, deletedAt: null },
      select: ['author', 'title', 'content', 'createdAt', 'updatedAt'],
    });
  }

  // 게시물 작성
  createArticle(title: string, content: string, password: number) {
    this.articleRepository.insert({
      author: 'test',
      title,
      content,
      // password도 일단은 잠시 숫자를 문자열로 바꿉니다.
      // 나중에 암호화된 비밀번호를 저장하도록 하겠습니다.
      password: password.toString(),
    });
  }

  // 게시물 수정
  async updateArticle(
    id: number,
    title: string,
    content: string,
    password: number,
  ) {
    await this.verfiyPassword(id, password);

    this.articleRepository.update(id, { title, content });
  }

  // 게시물 삭제
  async deleteArticle(id: number, password: number) {
    await this.verfiyPassword(id, password);

    // 바인딩
    this.articleRepository.softDelete(id);
  }

  private async verfiyPassword(id: number, password: number) {
    const article = await this.articleRepository.findOne({
      where: { id, deletedAt: null },
      select: ['password'],
    });

    if (_.isNil(article)) {
      throw new NotFoundException('Article not found. id :' + id);
    }

    if (article.password !== password.toString()) {
      throw new UnauthorizedException(`Password is not corrected. id : ${id}`);
    }
  }
}

- board.contorller.rs (최종)

import { Controller, Delete, Get, Post, Put } from '@nestjs/common';
import { Body, Param } from '@nestjs/common/decorators';
import { BoardService } from './board.service';
import { CreateArticleDto } from './create-article.dto';
import { DeleteArticleDto } from './delete-article.dto';
import { UpdateArticleDto } from './update-article.dto';

@Controller('board')
export class BoardController {
  // 서비스 주입
  constructor(private readonly boardService: BoardService) {}

  // 게시물 목록을 가져오는 API
  @Get('/articles')
  async getArticles() {
    return await this.boardService.getArticles();
  }

  // 게시물 상세보기 -> 게시물 ID로 확인
  @Get('/articles/:id')
  async getArticleById(@Param('id') articleId: number) {
    // string
    // class-validator, class0transformer
    return await this.boardService.getArticleById(articleId);
  }

  // 게시물 작성
  @Post('/articles')
  createArticle(@Body() data: CreateArticleDto) {
    return this.boardService.createArticle(
      data.title,
      data.content,
      data.password,
    );
  }

  // 게시물 수정
  @Put('/articles/:id')
  async updateArticle(
    @Param('id') articleId: number,
    @Body() data: UpdateArticleDto,
  ) {
    return await this.boardService.updateArticle(
      articleId,
      data.title,
      data.content,
      data.password,
    );
  }

  // 게시물 삭제
  @Delete('/articles/:id')
  async deleteArticle(
    @Param('id') articleId: number,
    @Body() data: DeleteArticleDto,
  ) {
    return await this.boardService.deleteArticle(articleId, data.password);
  }
}

! return 을 해야할 부분을 확실히 확인하고 return 해주자!

'내일배움캠프 4기 > TIL' 카테고리의 다른 글

DAY 69. nest.js 심화  (0) 2023.02.21
DAY 68. 이론 공부(3)  (2) 2023.02.21
DAY 66. TypeORM 실습  (0) 2023.02.16
DAY 65. 이론 공부(2)  (0) 2023.02.15
DAY 64. 이론 공부(1)  (0) 2023.02.14