Day4 - TypeORM & Repository

RINM·2023년 12월 25일

Set-up

저번 프로젝트처럼 도커 위에 postgres를 올려서 사용했다.

https://velog.io/@rinm/Day2-structure

왜 pgadmin 연결시 localhost로 하면 안되는지는 모르겠지만...

TypeORM

필요한 모듈을 설치해주자. nest.js에서는 @nestjs/typeorm으로 typeORM을 사용할 수 있다.

npm install pg typeorm @nestjs/typeorm --save

src/configs/typeorm.config.ts에 typeorm 설정을 만들어준다.

import { TypeOrmModuleOptions } from "@nestjs/typeorm";
import dotenv from 'dotenv'
dotenv.config()

export const typeORMConfig : TypeOrmModuleOptions = {
    //Database Type
    type: 'postgres',
    host: 'localhost',
    port: 5432,
    username: process.env.DB_USER_ID,
    password: process.env.DB_USER_PASSWORD,
    database: 'board-app',
    entities: [
        "src/entity/**/*.ts"
    ],
    synchronize: true,
}

루트 모듈 (app.module.ts)에서 typeorm을 import 시킨다.

Entity

이제 scr/entity에 board entity를 생성한다. 마찬가지로 이전 프로젝트를 참고한다.

https://velog.io/@rinm/Day3-Entity

import { BoardStatus } from "src/boards/board.model";
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";

@Entity()
export class Board extends BaseEntity {
    @PrimaryGeneratedColumn()
    id: number;

    @Column()
    title: string;

    @Column()
    description : string;

    @Column()
    status : BoardStatus;
}

Repository

repository는 entity 객체를 다루는데 사용된다. 즉, 귀찮은 쿼리를 repository에서 간단한 명령어로 처리한다고 생각하면 편하다. DB 작업이 있을 때 서비스에서 repository를 불러와 사용한다.
board.repository.ts를 작성한다. Repository를 받으면 커스텀 repository를 만들 수 있다.

typeORM 0.2와 typeORM 0.3의 커스텀 repository 생성 방식이 다르다. 0.2버전에서는 @EntityRepository() 데코레이터를 사용하여 생성할 수 있지만 0.3에서는 respository를 프로바이더로 생성하여 사용한다.

@Injectable()
export class BoardRepository extends Repository<Board> {
    constructor(private dataSource : DataSource){
        super(Board, dataSource.createEntityManager())
    }
}

super로 createEntityManager를 생성자에 넘겨주어야 Entity를 관리하는 레포지토리로서 사용할 수 있다.

생성한 repository를 board 모듈에서 provider로 등록시키기고 Entity는 import 해준다. 이렇게 forFeature로 등록된 entity는 typeORM 설정시 autoLoadEntities를 true로 해두면 경로 지정 없이 자동으로 로드된다.

@Module({
  controllers: [BoardsController],
  providers: [BoardsService,BoardRepository],
  imports: [TypeOrmModule.forFeature([Board])]
})

서비스에서 사용하려면 constructor를 사용한다. 컨트롤러에 서비스를 주입한 방식과 유사하다.

constructor(
    @InjectRepository(BoardRepository)
    private boardRepository : BoardRepository
){}

CRUD

TypeORM을 적용하여 메서드를 수정해준다. typeORM을 이용하여 데이터를 처리할 때에는 async - await 로직을 사용해야한다. 당연히 반환값 타입도 Promise가 된다.

Create

createBoard에서 typeORM의 create()를 사용하는 것으로 수정한다.

//Create new Board
async createBoard(createBoardDto: CreateBoardDto) : Promise <Board>{
    const {title, description} = createBoardDto
    const board = this.boardRepository.create({
        title,
        description,
        status: BoardStatus.PUBLIC
    })
    await this.boardRepository.save(board)
    return board;
}

생성한 board 인스턴스는 save()를 사용하여 DB에 저장해준다.

이제 repository 연결은 테스트했으니 repository로 필요한 로직들을 이동시킨다. createBoard() 메서드를 repository 내에서 정의하고 서비스에서는 이 메서드를 단순히 호출해주도록 수정한다.

//board.repository.ts
    async createBoard(createBoardDto: CreateBoardDto) : Promise <Board>{
        const {title, description} = createBoardDto
        const board = this.create({
            title,
            description,
            status: BoardStatus.PUBLIC
        })
        await this.save(board)
        return board;
    }
    
//boards.service.ts
    createBoard(createBoardDto: CreateBoardDto) : Promise <Board>{
        return this.boardRepository.createBoard(createBoardDto);
    }

Read

getBoardById를 위해서는 findOneBy()을 사용한다. async - await 처리에 주의한다.

//get board by id
async getBoardById(id:number): Promise <Board>{
    const found = await this.boardRepository.findOneBy({id})
    if(!found){
        throw new NotFoundException(`Cannot Find Board Id: ${id}`)
    }
    return found;
}

대응하는 컨트롤러도 수정한다. id가 number 타입이 된 것과 Promise를 반환하는 부분만 신경써주면 된다.

@Get('/:id') //Get a board by id
getBoardById(@Param('id') id :number): Promise<Board> {
    return this.boardService.getBoardById(id)
}

getAllBoard에서는 typeORM의 find를 사용해서 boards 테이블의 모든 board를 가져올 수 있다.

async getAllBoards() : Promise<Board[]> {
    return this.boardRepository.find();
}

컨트롤러에서는 리턴 타입만 수정해주면 된다.

@Get() //Get all board list
getAllBoard() : Promise<Board[]> {
    return this.boardService.getAllBoards();
}

Update

updateBoardStatus는 기존과 유사하게 DB에서 원하는 id의 board를 가져온 후 status값을 변경하고 다시 DB에 저장하는 로직을 사용한다.

async updateBoardStatus(id: number, status: BoardStatus) : Promise<Board> {
    const board = await this.getBoardById(id);
    board.status = status;
    await this.boardRepository.save(board)
    return board;
}

DB에서도 변경 사항을 확인한다.

1번 board가 수정 요청을 받아 status가 PRIVATE으로 변경되었다.

Delete

typeORM에는 삭제를 위해 remove와 delete 두 가지 함수를 제공한다. delete는 존재하지 않는 아이템을 삭제하려 하면 아무런 일이 일어나지 않지만 remove에서는 404 오류를 발생시킨다. delete 함수를 사용하여 deleteBoard를 처리해준다.

async deleteBoard(id:number): Promise<void> {
    const result = await this.boardRepository.delete(id)

    console.log('[delete]',result);
}

리턴 값이 딱히 없으니 로그를 찍어준다.

id가 number로 바뀌었으니 컨트롤러에서도 수정해준다. ParseIntPipe를 사용하면 더 좋다.

@Delete('/:id') //Delete a board by id
deleteBoard(@Param('id',ParseIntPipe) id : number) : Promise<void> {
    return this.boardService.deleteBoard(id)
}

Pgadmin으로 DB를 확인한다.

여기서 id가 3인 board 삭제 요청을 보내면 다음과 같이 DB에서도 잘 삭제된 것을 볼 수 있다.

예외처리도 해주자. 없는 id에 삭제 요청이 들어오는 경우 delete는 딱히 error를 발생시키지 않지만 반환값의 affected가 0으로 뜬다. 이때를 기점으로 삭제할 수 없는 id라는 에러메시지를 반환하는 로직을 추가한다.

async deleteBoard(id:number): Promise<void> {
    const result = await this.boardRepository.delete(id)
    if(result.affected===0){
        throw new NotFoundException(`Cannot Find Board Id: ${id}`)
    }
    console.log('[delete]',result);
}

0개의 댓글