본문 바로가기
TIL

TIL 240627 - TypeORM 적용

by lemonpie611 2024. 6. 27.

1. 이전에 만든 게시판 프로젝트에 TypeORM 적용하기

1) TypeORM : Nest.js에서 데이터베이스를 연동하는 수단

 

2) TypeORM 적용 준비

  • @nestjs/config, joi 패키지 설치
npm i @nestjs/config joi

 

3) app.module.ts에서 DB 생성을 위한 코드 작성

//app.module.ts

import Joi from 'joi';

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule, TypeOrmModuleOptions } from '@nestjs/typeorm';

import { AppController } from './app.controller';
import { AppService } from './app.service';
import { Post } from './post/entities/post.entity';
import { PostModule } from './post/post.module';

const typeOrmModuleOptions = {
  useFactory: async (
    configService: ConfigService,
  ): Promise<TypeOrmModuleOptions> => ({
    type: 'mysql',
    host: configService.get('DB_HOST'),
    port: configService.get('DB_PORT'),
    username: configService.get('DB_USERNAME'),
    password: configService.get('DB_PASSWORD'),
    database: configService.get('DB_NAME'),
    entities: [Post],
    synchronize: configService.get('DB_SYNC'),
    logging: true,
  }),
  inject: [ConfigService],
};

@Module({
  imports: [
    ConfigModule.forRoot({
      isGlobal: true,
      validationSchema: Joi.object({
        DB_HOST: Joi.string().required(),
        DB_PORT: Joi.number().required(),
        DB_USERNAME: Joi.string().required(),
        DB_PASSWORD: Joi.string().required(),
        DB_NAME: Joi.string().required(),
        DB_SYNC: Joi.boolean().required(),
      }),
    }),
    TypeOrmModule.forRootAsync(typeOrmModuleOptions),
    PostModule,
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}
  • AppModule의 Imports에 ConfigModule 추가
    • isGlobal:true : 전역적으로 사용한다고 선언
    • validationSchema : 정의된 스키마대로 .env가 정의되어있지 않으면 서버는 동작하지 않음
  • typeOrmModuleOptions 옵션
    • 변수는 TypeORM 모듈의 옵션을 의미
    • 하드코딩된 설정을 TypeORM에 주입하지 않고, 동적으로 .env 파일에서 설정을 불러와서 주입 (즉, 값을 그대로 때려박는게 아니라 .env파일에서 불러옴)
    • 따라서 useFactory 방법 사용 (TypeOrmModule.forRootAsync 함수에 전달되는 설정 객체를 동적으로 생성하기 위해 사용) > inject: [ConfigService] 옵션을 통해 useFactory에서 사용할 ConfigService를 의존성 주입
  • .forRootAsync 와 forRoot의 차이
    • forRootAsync는 동적으로 TypeORM 설정을 주입할 때 사용
    • forRoot는 하드코딩된 TypeORM 설정을 사용할 때 사용, 잘 안씀

3) 엔티티, 레포지토리 생성

  • 엔티티 : 데이터베이스의 특정 테이블을 대표하는 객체, 테이블의 각 로우를 객체로 표현
  • 리포지토리 : 엔티티와 데이터베이스 간의 중간 계층을 형성하는 객체, DB와의 통신과정을 알지 못해도 추상화된 리포지토리 함수를 사용해서 DB에서 원하는 결과를 얻을 수 있도록 함
//post.entity.ts
import { IsNumber, IsString } from 'class-validator';
import {
  Column,
  CreateDateColumn,
  DeleteDateColumn,
  Entity,
  PrimaryGeneratedColumn,
  UpdateDateColumn,
} from 'typeorm';

@Entity({
  name: 'posts',
})
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

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

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

  @IsNumber()
  @Column('int', { select: false, nullable: false })
  password: number;

  @CreateDateColumn()
  createdAt: Date;

  @UpdateDateColumn()
  updatedAt: Date;

  @DeleteDateColumn()
  deletedAt?: Date;
}
  • @Entity : 해당 클래스가 어떤 테이블에 매핑되는지를 나타내는 어노테이션
  • PK에 해당하는 컬럼에는 @PrimaryGeneratedColumn 어노테이션 사용
  • 나머지 컬럼에는 @Column, 날짜에 해당하면 @DateColumn 어노테이션 사용
  • 비밀번호의 select 옵션 false : 일반적인 조회로는 비밀번호를 알아낼 수 없도록 하여 민감한 정보 보호
  • @DeleteDateColumn : 삭제된 날짜를 기록, 이 어노테이션을 사용하면 엔티티 삭제 순간 실제로 삭제되지 않고 논리적 삭제됨 > 전체 게시물을 가져올 때 deletedAt != null인 게시물만 가져옴
//post.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';

import { Post } from './entities/post.entity';
import { PostController } from './post.controller';
import { PostService } from './post.service';

@Module({
  imports: [TypeOrmModule.forFeature([Post])],
  controllers: [PostController],
  providers: [PostService],
})
export class PostModule {}
  • @Module 데코레이터의 import 속성에 사용할 레포지토리를 넣어줌
//post.service.js

import _ from 'lodash';
import { Repository } from 'typeorm';

import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { CreatePostDto } from './dto/create-post.dto';
import { RemovePostDTO } from './dto/remove-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Post } from './entities/post.entity';

@Injectable()
export class PostService {
  private articles: { id: number; title: string; content: string }[] = [];
  private articlePasswords = new Map<number, number>();

  constructor(
    @InjectRepository(Post) private postRepository: Repository<Post>,
  ) {}
  .
  .
  .
  • 생성자 추가 > @InjectRepository 어노테이션을 사용하여 리포지토리 주입

4) 서비스 리팩토링

import _ from 'lodash';
import { Repository } from 'typeorm';

import {
  Injectable,
  NotFoundException,
  UnauthorizedException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';

import { CreatePostDto } from './dto/create-post.dto';
import { RemovePostDTO } from './dto/remove-post.dto';
import { UpdatePostDto } from './dto/update-post.dto';
import { Post } from './entities/post.entity';

@Injectable()
export class PostService {
  constructor(
    @InjectRepository(Post) private postRepository: Repository<Post>,
  ) {}

  async create(createPostDto: CreatePostDto) {
    return (await this.postRepository.save(createPostDto)).id;
  }

  async findAll() {
    return await this.postRepository.find({
      where: { deletedAt: null },
      select: ['id', 'title', 'updatedAt'],
    });
  }

  async findOne(id: number) {
    return await this.postRepository.findOne({
      where: { id, deletedAt: null },
      select: ['title', 'content', 'updatedAt'],
    });
  }

  async update(id: number, updatePostDto: UpdatePostDto) {
    const { content, password } = updatePostDto;
    const post = await this.postRepository.findOne({
      select: ['password'],
      where: { id },
    });

    if (_.isNil(post)) {
      throw new NotFoundException('게시물을 찾을 수 없습니다.');
    }

    if (!_.isNil(post.password) && post.password !== password) {
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
    }

    await this.postRepository.update({ id }, { content });
  }

  async remove(id: number, removePostDto: RemovePostDTO) {
    const { password } = removePostDto;

    const post = await this.postRepository.findOne({
      select: ['password'],
      where: { id },
    });

    if (_.isNil(post)) {
      throw new NotFoundException('게시물을 찾을 수 없습니다.');
    }

    if (!_.isNil(post.password) && post.password !== password) {
      throw new UnauthorizedException('비밀번호가 일치하지 않습니다.');
    }

    return this.postRepository.softDelete({ id });
  }
}

 

5) RDS 사용할 경우 .env 설정

DB_HOST="express-database....(엔드포인트 주소)"
DB_PORT=3306
DB_USERNAME="데이터베이스 계정"
DB_PASSWORD="데이터베이스 암호"
DB_NAME="nestjsboard"
DB_SYNC=true

로컬에서 사용할거면 DB_HOST에 localhost라고 입력

RDS 생성시 설정했던 username, password 입력, 기억안나면 이전 node.js 프로젝트의 .env 파일 뒤져보자.... 엔드포인트 앞에 있는 2개 그대로 들고오기