TypeORM 트랜잭션 사용 방법

TypeORM 에서 제공하는 Transaction 종류에 대해 알아보려고 합니다. 관련해서 0.2x 버전과 0.3x 버전을 간단한 예시와 함께 비교해 보겠습니다.

typeOrm 0.2x

Connection 또는 Entity Manage 사용

  • connection 이나 EntityManager의 transaction 함수를 이용하는 것입니다
  • 주의 점은 제공받은 EntityManager 를 사용해야 합니다 getManager를 통해 다른 매니저를 사용하여 조작하면 롤백 반영이 되지 않습니다
  • 첫번째 인자로 격리성 수준 설정이 가능합니다
import {
Connection,
EntityManager,
getConnection,
getManager,
Repository,
Transaction,
TransactionManager,
TransactionRepository,
} from "typeorm"

...
constructor( private connection: Connection) {}
...
public async examTransactionGetConnection() {
// 격리성 수준 설정은 1번째 인자로 할수있습니다
// IsolationLevel = "READ UNCOMMITTED" | "READ COMMITTED" | "REPEATABLE READ" | "SERIALIZABLE";
await getConnection().transaction( async trasactionalEntityManager => {
const postTransfer = new PostTransfer()
postTransfer.id = uuid.v4()
postTransfer.areaDanjiId = 1
postTransfer.targetDanjiId = "1"
postTransfer.targetDanjiName = "테스트아파트1"
postTransfer.targetServiceType = TargetServiceType.MOVILL
postTransfer.status = PostTransferStatus.ING
postTransfer.userId = 1
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer)

//throw Error("강제 에러")

const postTransfer2 = new PostTransfer()
postTransfer2.id = uuid.v4()
postTransfer2.areaDanjiId = 2
postTransfer2.targetDanjiId = "2"
postTransfer2.targetDanjiName = "테스트아파트2"
postTransfer2.targetServiceType = TargetServiceType.MOVILL
postTransfer2.status = PostTransferStatus.ING
postTransfer2.userId = 2
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer2)
})
}

public async examTransactionGetManager() {
// 격리성 수준 설정은 1번째 인자로 할수있습니다
// IsolationLevel = "READ UNCOMMITTED" | "READ COMMITTED" | "REPEATABLE READ" | "SERIALIZABLE";
await getManager().transaction( async trasactionalEntityManager => {
const postTransfer = new PostTransfer()
postTransfer.id = uuid.v4()
postTransfer.areaDanjiId = 1
postTransfer.targetDanjiId = "1"
postTransfer.targetDanjiName = "테스트아파트1"
postTransfer.targetServiceType = TargetServiceType.MOVILL
postTransfer.status = PostTransferStatus.ING
postTransfer.userId = 1
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer)

//throw Error("강제 에러")

const postTransfer2 = new PostTransfer()
postTransfer2.id = uuid.v4()
postTransfer2.areaDanjiId = 2
postTransfer2.targetDanjiId = "2"
postTransfer2.targetDanjiName = "테스트아파트2"
postTransfer2.targetServiceType = TargetServiceType.MOVILL
postTransfer2.status = PostTransferStatus.ING
postTransfer2.userId = 2
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer2)
})
}

QueryRunner 사용

  • startTransaction(isolationLevel) 트랜잭션 시작
  • commitTransaction() 트랜잭션 커밋
  • rollbackTransaction() 트랜잭션 롤백
  • release() 인스턴스
import {
Connection,
EntityManager,
getConnection,
getManager,
Repository,
Transaction,
TransactionManager,
TransactionRepository,
} from "typeorm"

...
constructor( private connection: Connection) {}
...

public async examTransactionQueryRunner() {

// const connection = getConnection()
const queryRunner = this.connection.createQueryRunner()
await queryRunner.connect()

await queryRunner.startTransaction()

const postTransfer = new PostTransfer()
postTransfer.id = uuid.v4()
postTransfer.areaDanjiId = 1
postTransfer.targetDanjiId = "1"
postTransfer.targetDanjiName = "테스트아파트1"
postTransfer.targetServiceType = TargetServiceType.MOVILL
postTransfer.status = PostTransferStatus.ING
postTransfer.userId = 1

const postTransfer2 = new PostTransfer()
postTransfer2.id = uuid.v4()
postTransfer2.areaDanjiId = 2
postTransfer2.targetDanjiId = "2"
postTransfer2.targetDanjiName = "테스트아파트2"
postTransfer2.targetServiceType = TargetServiceType.MOVILL
postTransfer2.status = PostTransferStatus.ING
postTransfer2.userId = 2
try {

await queryRunner.manager.getRepository(PostTransfer).save(postTransfer)
//throw Error("강제 에러")
await queryRunner.manager.getRepository(PostTransfer).save(postTransfer2)
// 성공하면 commitTransaction
await queryRunner.commitTransaction()

} catch (err) {
// 실패하면 롤백
await queryRunner.rollbackTransaction()
} finally {
// 연결 해제
await queryRunner.release()
}
}

데코레이터 사용

  • @Transaction - 모든 실행을 단일 데이터베이스 변환으로 래핑합니다.
  • @TransactionManager - 트랜잭션 내에서 쿼리를 실행하는 데 사용됩니다.
  • @TransactionRepository - 저장소에 트랜잭션을 주입하는 데 사용됩니다.
import {
Connection,
EntityManager,
getConnection,
getManager,
Repository,
Transaction,
TransactionManager,
TransactionRepository,
} from "typeorm"

...
constructor( private connection: Connection) {}
...
@Transaction({ isolation: "SERIALIZABLE" })
public async examTransactionDecorators1(@TransactionManager() manager: EntityManager) {

const postTransfer = new PostTransfer()
postTransfer.id = uuid.v4()
postTransfer.areaDanjiId = 1
postTransfer.targetDanjiId = "1"
postTransfer.targetDanjiName = "테스트아파트1"
postTransfer.targetServiceType = TargetServiceType.MOVILL
postTransfer.status = PostTransferStatus.ING
postTransfer.userId = 1
await manager.getRepository(PostTransfer).save(postTransfer)

//throw Error("강제 에러")

const postTransfer2 = new PostTransfer()
postTransfer2.id = uuid.v4()
postTransfer2.areaDanjiId = 2
postTransfer2.targetDanjiId = "2"
postTransfer2.targetDanjiName = "테스트아파트2"
postTransfer2.targetServiceType = TargetServiceType.MOVILL
postTransfer2.status = PostTransferStatus.ING
postTransfer2.userId = 2
await manager.getRepository(PostTransfer).save(postTransfer2)

}

데코레이더 관련 지원 외부 라이브러리 (https://github.com/odavid/typeorm-transactional-cls-hooked)

typeOrm 0.3x

  • Connection , Entity Manage 는 Deprecated
  • 데코레이터 삭제

DataSource 또는 EntityManage 사용

import { DataSource } from "typeorm"
...
constructor( private dataSource: DataSource) {}
...
public async examTransactionGetDataSoruce() {
// 1. Transaction method
// 예제에서 볼수 있듯이 격리성 수준 설정은 1번째 인자로 할수있다
// IsolationLevel = "READ UNCOMMITTED" | "READ COMMITTED" | "REPEATABLE READ" | "SERIALIZABLE";
await this.dataSource.transaction( async trasactionalEntityManager => {

const postTransfer = new PostTransfer()
postTransfer.id = uuid.v4()
postTransfer.areaDanjiId = 1
postTransfer.targetDanjiId = "1"
postTransfer.targetDanjiName = "테스트아파트1"
postTransfer.targetServiceType = TargetServiceType.MOVILL
postTransfer.status = PostTransferStatus.ING
postTransfer.userId = 1
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer)

throw Error("강제 에러")

const postTransfer2 = new PostTransfer()
postTransfer2.id = uuid.v4()
postTransfer2.areaDanjiId = 2
postTransfer2.targetDanjiId = "2"
postTransfer2.targetDanjiName = "테스트아파트2"
postTransfer2.targetServiceType = TargetServiceType.MOVILL
postTransfer2.status = PostTransferStatus.ING
postTransfer2.userId = 2
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer2)
})
}

public async examTransactionGetDataSoruceManager() {
// 1. Transaction method
// 예제에서 볼수 있듯이 격리성 수준 설정은 1번째 인자로 할수있다
// IsolationLevel = "READ UNCOMMITTED" | "READ COMMITTED" | "REPEATABLE READ" | "SERIALIZABLE";
await this.dataSource.manager.transaction( async trasactionalEntityManager => {
const postTransfer = new PostTransfer()
postTransfer.id = uuid.v4()
postTransfer.areaDanjiId = 1
postTransfer.targetDanjiId = "1"
postTransfer.targetDanjiName = "테스트아파트1"
postTransfer.targetServiceType = TargetServiceType.MOVILL
postTransfer.status = PostTransferStatus.ING
postTransfer.userId = 1
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer)

throw Error("강제 에러")

const postTransfer2 = new PostTransfer()
postTransfer2.id = uuid.v4()
postTransfer2.areaDanjiId = 2
postTransfer2.targetDanjiId = "2"
postTransfer2.targetDanjiName = "테스트아파트2"
postTransfer2.targetServiceType = TargetServiceType.MOVILL
postTransfer2.status = PostTransferStatus.ING
postTransfer2.userId = 2
await trasactionalEntityManager.getRepository(PostTransfer).save(postTransfer2)
})
}

QueryRunner 사용

import { DataSource } from "typeorm"
...
constructor( private dataSource: DataSource) {}
...
public async examTransactionQueryRunner() {

//
const queryRunner = this.dataSource.createQueryRunner()
await queryRunner.connect()

await queryRunner.startTransaction()

const postTransfer = new PostTransfer()
postTransfer.id = uuid.v4()
postTransfer.areaDanjiId = 1
postTransfer.targetDanjiId = "1"
postTransfer.targetDanjiName = "테스트아파트1"
postTransfer.targetServiceType = TargetServiceType.MOVILL
postTransfer.status = PostTransferStatus.ING
postTransfer.userId = 1

const postTransfer2 = new PostTransfer()
postTransfer2.id = uuid.v4()
postTransfer2.areaDanjiId = 2
postTransfer2.targetDanjiId = "2"
postTransfer2.targetDanjiName = "테스트아파트2"
postTransfer2.targetServiceType = TargetServiceType.MOVILL
postTransfer2.status = PostTransferStatus.ING
postTransfer2.userId = 2
try {

await queryRunner.manager.getRepository(PostTransfer).save(postTransfer)
//throw Error("강제 에러")
await queryRunner.manager.getRepository(PostTransfer).save(postTransfer2)
// 성공하면 commitTransaction
await queryRunner.commitTransaction()

} catch (err) {
// 실패하면 롤백
await queryRunner.rollbackTransaction()
} finally {
// 연결 해제
await queryRunner.release()
}
}

데코레이더 삭제

  • typeorm 에서 권장하는 방법은 아니지만 외부 라이브러리를 사용하면 데코레이터를 사용할수 있습니다
  • typeorm-transactional-cls-hooked(0.2x만지원) 기반으로 fork 0.3 지원(https://github.com/Aliheym/typeorm-transactional)

마치며

지금까지 간략한 예제로 TypeORM transaction 에 대해 알아 보았습니다. 0.3x 버전으로 변경 되면서 많은 변화가 있는것으로 보입니다. 예제가 도움이 되었으면 좋겠습니다. 감사합니다.

참고자료

https://www.tutorialspoint.com/typeorm/typeorm_transactions.htm (0.2 버전 가이드)

https://typeorm.io/transactions

https://typeorm.biunav.com/en/changelog.html#_0-3-7-2022-06-29 (버전별 변경사항)

--

--

사람들이 공간을 경험하는 방식을 바꾸고자 합니다.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store