TypeORM의 Transaction 사용하기
이번 포스팅은 TypeORM의 transaction 사용법에 대한 내용이다. 간단한 사용법이지만 정리하는 차원에 작성한다.
프로젝트는 nestjs 기반으로 생성했으며, 엔티티 프레임워크로 typeorm과 데이터베이스는 mysql을 사용했다. transaction 수행을 위해 간단한 요구사항 하나를 작성했다.
- 사용자가 탈퇴(삭제)하면, 작성한 게시글은 전부 삭제한다.
위의 요구사항에서 알 수 있듯이, 어떠한 이유로든 사용자의 탈퇴가 실패하게 되면 삭제된 게시글은 transaction에 의해 롤백 되어야 한다.
Entity
엔티티는 User(사용자), Board(게시글) 두 개로 정의했으며, 1:N의 관계를 맺는다.
import { Entity, Column, PrimaryGeneratedColumn, OneToMany, Timestamp, CreateDateColumn } from 'typeorm';
import { Board } from './Board';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@CreateDateColumn()
createdAt: Date;
@CreateDateColumn()
updatedAt: Date;
@OneToMany((type) => Board, (board) => board.user)
boards: Board[]
}
import { Entity, Column, PrimaryGeneratedColumn, ManyToOne, Timestamp, CreateDateColumn } from 'typeorm';
import { User } from './User';
@Entity()
export class Board {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ length: 500 })
contents: string;
@CreateDateColumn()
createdAt: Date;
@CreateDateColumn()
updatedAt: Date;
@ManyToOne((type) => User, (user) => user.boards)
user: User
}
Controller
컨트롤러에서 사용자 삭제 API를 호출하며, UserService의 deleteUserAndBoards 메소드를 호출한다.
import { Controller, Inject, Get, Param, Delete } from "@nestjs/common"
import { UserService } from "src/services/UserService"
@Controller()
export class UserController {
@Inject() private readonly userService: UserService
@Delete('/users/:id')
async deleteUser(@Param('id') userId: number) {
return await this.userService.deleteUserAndBoards(userId)
}
}
Service
컨트롤러에서 호출한 deleteUserAndBoards
는 UserService
에 정의되어있으며, 이는 transaction을 이용해 사용자의 삭제 및 게시글 삭제를 수행한다.
typeorm의 transaction은 Connection이나 EntityManager를 통해 수행되야하며, 아래 코드와 같이 반드시 transactionalEntityManager(EntityManager)
를 통해 콜백함수 내부에서 진행되어야 한다.
아래의 코드에서는 EntityManager와 쿼리 수행에 필요한 값을 인자로 전달하고, 해당 Repository에서 EntityManager를 통해 데이터베이스 작업을 수행했다.
import { BadRequestException, Inject, Injectable, InternalServerErrorException } from '@nestjs/common'
import { InjectRepository } from '@nestjs/typeorm'
import { getManager, getConnection } from 'typeorm'
import { Board } from 'src/entities/Board'
import { User } from 'src/entities/User'
import { UserRepository } from 'src/repositories/UserRepository'
import { BoardRepository } from 'src/repositories/BoardRepository'
@Injectable()
export class UserService {
@InjectRepository(User) private readonly userRepository: UserRepository
@InjectRepository(Board) private readonly boardRepository: BoardRepository
async deleteUserAndBoards(userId: number) {
await getManager().transaction(async (transactionalEntityManager) => {
await this.boardRepository.deleteBoardsByUserId(transactionalEntityManager, userId)
await this.userRepository.deleteUserByUserId(transactionalEntityManager, userId)
}).catch((err) => {
throw err
})
}
}
위와 같은 방식 이외에도, QueryRunner를 통해서 transaction을 수행할 수 있다. QueryRunner를 이용하면 아래와 같이 commit, rollback, release의 transaction 상태를 수동으로 제어할 수 있다.
async deleteUserAndBoards(userId: number) {
const queryRunner = await getConnection().createQueryRunner()
await queryRunner.startTransaction()
try {
await this.boardRepository.deleteBoardsByUserId(queryRunner.manager, userId)
await this.userRepository.deleteUserByUserId(queryRunner.manager, userId)
await queryRunner.commitTransaction();
} catch(err) {
await queryRunner.rollbackTransaction();
} finally {
await queryRunner.release();
}
}
Repository
레포지토리에서는 서비스에서 인자로 전달받은 EntityManager를 통해 쿼리를 수행한다.
import { EntityRepository, Repository, TransactionManager, EntityManager } from "typeorm"
import { User } from "src/entities/User"
@EntityRepository(User)
export class UserRepository extends Repository<User> {
async deleteUserByUserId(@TransactionManager() transactionManager: EntityManager, userId: number) {
return await transactionManager.delete(User, userId)
}
}
import { EntityRepository, Repository, TransactionManager, EntityManager } from "typeorm"
import { Board } from "src/entities/Board"
@EntityRepository(Board)
export class BoardRepository extends Repository<Board> {
async deleteBoardsByUserId(@TransactionManager() transactionManager: EntityManager, userId: number) {
return await transactionManager.delete(Board, { user: userId })
}
}
실행
curl을 통해 사용자 삭제 API를 호출했으며, 의도적인 exception을 발생시켰다.
curl -X DELETE http://localhost:3000/users/1
그 결과, transaction에 의해 롤백이 잘 동작하는 것을 확인할 수 있다.
query: START TRANSACTION
query: DELETE FROM `board` WHERE `user_id` = ? -- PARAMETERS: ["1"]
query: DELETE FROM `user` WHERE `id` IN (?) -- PARAMETERS: ["1"]
query: ROLLBACK