TypeORM 트랜잭션(Transaction) 제어 with Query Runner 1일차

박재하·2023년 12월 1일
0

목차

  • 트랜잭션 적용
    • PATCH /post/:id/like, PATCH /post/:id/unlike
    • PATCH /post/:id, DELETE /post/:id
    • 트랜잭션 구현
    • DELETE /post/:id에 적용
    • PATCH /post/:id에 적용
    • 동작 화면

트랜잭션 적용

필요한 로직에 트랜잭션을 적용해보자.

PATCH /post/:id/like, PATCH /post/:id/unlike

스크린샷 2023-11-29 오후 5 18 40

사진 하단을 보면 UPDATE, DELETE가 일어나는 로직에 이미 내부적으로
Transaction 처리가 되어있는 것을 확인할 수 있다. 불필요하므로 패스.

PATCH /post/:id, DELETE /post/:id

스크린샷 2023-11-29 오후 5 28 05 스크린샷 2023-11-29 오후 5 21 58

게시글 수정 및 삭제 로직은 이미지에 대한 삭제, 삽입과
Board에 대한 수정, 삭제가 다른 트랜잭션으로 돌아서 합치면 좋을 듯 하다.

트랜잭션 구현

DELETE /post/:id 로직을 트랜잭션 방식으로 구현해보자.

학습메모 1을 참고하여 아래와 같은 형식으로 하면 되시겠다.

// create a new query runner
const queryRunner = dataSource.createQueryRunner();

// establish real database connection using our new query runner
await queryRunner.connect();

// now we can execute any queries on a query runner, for example:
await queryRunner.query('SELECT * FROM users');

// we can also access entity manager that works with connection created by a query runner:
const users = await queryRunner.manager.find(User);

// lets now open a new transaction:
await queryRunner.startTransaction();

try {
	// execute some operations on this transaction:
	await queryRunner.manager.save(user1);
	await queryRunner.manager.save(user2);
	await queryRunner.manager.save(photos);

	// commit transaction now:
	await queryRunner.commitTransaction();
} catch (err) {
	// since we have errors let's rollback changes we made
	await queryRunner.rollbackTransaction();
} finally {
	// you need to release query runner which is manually created:
	await queryRunner.release();
}

dataSource는 멘토링 일지(학습메모 2) 참고하여 아래와같이

image

constructor(private readonly dataSource: DataSource) {}

추가해주고 사용하면 됨.

DELETE /post/:id에 적용

// board.service.ts
constructor(
  ...
  @InjectDataSource()
  private readonly dataSource: DataSource,
) {}
...
async deleteBoard(id: number, userData: UserDataDto): Promise<void> {
  // transaction 생성하여 board, image, star, like 테이블 동시에 삭제
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();

  // const board: Board = await this.boardRepository.findOneBy({ id });
  const board: Board = await queryRunner.manager.findOneBy(Board, { id });

  if (!board) {
    throw new NotFoundException(`Not found board with id: ${id}`);
  }

  // 게시글 작성자와 삭제 요청자가 다른 경우
  if (board.user.id !== userData.userId) {
    throw new BadRequestException('You are not the author of this post');
  }

  // transaction 시작
  await queryRunner.startTransaction();
  try {
    // 연관된 이미지 삭제
    for (const image of board.images) {
      // 이미지 리포지토리에서 삭제
      // await this.imageRepository.delete({ id: image.id });
      await queryRunner.manager.delete(Image, { id: image.id });
      // NCP Object Storage에서 삭제
      await this.deleteFile(image.filename);
    }

    // 연관된 별 스타일 삭제
    if (board.star) {
      await this.starModel.deleteOne({ _id: board.star });
    }

    // like 조인테이블 레코드들은 자동으로 삭제됨 (외래키 제약조건 ON DELETE CASCADE)

    // 게시글 삭제
    // const result = await this.boardRepository.delete({ id });
    const result = await queryRunner.manager.delete(Board, { id });

    // commit Transaction
    await queryRunner.commitTransaction();
  } catch (err) {
    Logger.error(err);
    await queryRunner.rollbackTransaction();
  } finally {
    await queryRunner.release();
  }
}

조회와 에러처리 외에 실제 삭제하는 메인 로직은 try안에 넣고,
전후에 startTransaction, commitTransaction, rollbackTransaction, release 등을 공식문서대로 적절히 배치해준다.

기존 쿼리들을 모두 queryRunner 안에서 실행시키도록, queryRunner.manager 메소드 아래에서 실행한다.

스크린샷 2023-11-29 오후 5 48 48

만들어둔 포스트 삭제

스크린샷 2023-11-29 오후 5 44 26

진짜 되네? 야호!

PATCH /post/:id에 적용

이번엔 업데이트 로직.

async updateBoard(
  id: number,
  updateBoardDto: UpdateBoardDto,
  userData: UserDataDto,
  files: Express.Multer.File[],
) {
  // transaction 생성하여 board, image, star, like 테이블 동시에 수정
  const queryRunner = this.dataSource.createQueryRunner();
  await queryRunner.connect();

  // const board: Board = await this.boardRepository.findOneBy({ id });
  const board: Board = await queryRunner.manager.findOneBy(Board, { id });
  if (!board) {
    throw new NotFoundException(`Not found board with id: ${id}`);
  }

  // 게시글 작성자와 수정 요청자가 다른 경우
  if (board.user.id !== userData.userId) {
    throw new BadRequestException('You are not the author of this post');
  }

  // star에 대한 수정은 별도 API(PATCH /star/:id)로 처리하므로 400 에러 리턴
  if (updateBoardDto.star) {
    throw new BadRequestException(
      'You cannot update star with this API. use PATCH /star/:id',
    );
  }

  // transaction 시작
  await queryRunner.startTransaction();
  try {
    if (files.length > 0) {
      const images: Image[] = [];
      for (const file of files) {
        const image = await this.uploadFile(file);
        images.push(image);
      }
      // 기존 이미지 삭제
      for (const image of board.images) {
        // 이미지 리포지토리에서 삭제
        // await this.imageRepository.delete({ id: image.id });
        await queryRunner.manager.delete(Image, { id: image.id });
        // NCP Object Storage에서 삭제
        await this.deleteFile(image.filename);
      }
      // 새로운 이미지로 교체
      board.images = images;
    }

    // updateBoardDto.content가 존재하면 AES 암호화하여 저장
    if (updateBoardDto.content) {
      updateBoardDto.content = encryptAes(updateBoardDto.content);
    }

    // const updatedBoard: Board = await this.boardRepository.save({
    // 	...board,
    // 	...updateBoardDto,
    // });
    const updatedBoard: Board = await queryRunner.manager.save(Board, {
      ...board,
      ...updateBoardDto,
    });

    // commit Transaction
    await queryRunner.commitTransaction();

    delete updatedBoard.user.password; // password 제거하여 반환
    return updatedBoard;
  } catch (err) {
    Logger.error(err);
    await queryRunner.rollbackTransaction();
  } finally {
    await queryRunner.release();
  }
}
스크린샷 2023-11-29 오후 6 02 47

만들어둔 포스트 변경

스크린샷 2023-11-29 오후 6 02 37

반영은 잘 되는데, upload image 내에 있는 새로운 이미지에 대한 레코드 생성은 queryRunner로 돌지 못해서 별도의 트랜잭션이 도는 것을 확인할 수 있다.

이 부분은 별도로 개선해도 좋을 것 같다!

동작 화면

DELETE /post/:id에 적용

  • before
스크린샷 2023-11-29 오후 5 21 58
  • after
스크린샷 2023-11-29 오후 5 44 26

PATCH /post/:id에 적용

  • before
스크린샷 2023-11-29 오후 5 28 05
  • after
스크린샷 2023-11-29 오후 6 02 37

학습메모

  1. TypeORM Transactions
  2. dataSource 활용 (week2 멘토링일지)
profile
해커 출신 개발자

0개의 댓글