필요한 로직에 트랜잭션을 적용해보자.
사진 하단을 보면 UPDATE, DELETE가 일어나는 로직에 이미 내부적으로
Transaction 처리가 되어있는 것을 확인할 수 있다. 불필요하므로 패스.
게시글 수정 및 삭제 로직은 이미지에 대한 삭제, 삽입과
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) 참고하여 아래와같이
constructor(private readonly dataSource: DataSource) {}
추가해주고 사용하면 됨.
// 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 메소드 아래에서 실행한다.
만들어둔 포스트 삭제
진짜 되네? 야호!
이번엔 업데이트 로직.
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();
}
}
만들어둔 포스트 변경
반영은 잘 되는데, upload image 내에 있는 새로운 이미지에 대한 레코드 생성은 queryRunner로 돌지 못해서 별도의 트랜잭션이 도는 것을 확인할 수 있다.
이 부분은 별도로 개선해도 좋을 것 같다!