[NestJS] TypeORM — Relation, Transactions

JeungJoo Lee
CrocusEnergy
Published in
12 min readSep 17, 2020

지난 시간에는 [NestJS] TypeORM 기본 CRUD 작성하기 를 살펴보았다. 이번 시간에는 Entity 간의 Relation 을 어떻게 TypeORM 을 통해 맺는지와 트랜잭션 처리를 어떻게 하는지를 알아보도록 하겠다.

Relations (엔티티 관계)

RDB 의 경우 엔티티 간의 관계가 Primary Key 와 Foreign Key 를 통해 설정되는데 해당 부분을 살펴 보도록 하자.

1 : 1 관계

Primary Key 를 가지고 있는 하나의 테이블 안에 모든 row data 에서 자신 또는 다른 테이블의 하나의 row data 의 Foreign Key 로 관계를 맺을 수 있는 것을 말한다. 관계의 정의는 데코레이터로 표현이 가능하다.

1 : N , N : 1 관계

Primary Key 를 가지고 있는 하나의 테이블 안에 하나의 row data 에서 자신 또는 다른 테이블의 하나 이상의 row data 의 Foreign Key 로 관계를 맺을 수 있는 관계를 말한다. 관계의 정의는 데코레이터로 표현이 가능하다.

N : N 관계

Primary Key 를 가지고 있는 하나의 테이블 안에 하나 이상의 row data 에서 자신 또는 다른 테이블의 하나 이상의 row data 의 Foreign Key 로 관계를 맺을 수 있는 관계를 말한다. 관계의 정의는 데코레이터로 표현이 가능하다.

예시로 먼저 관계설정을 위해 아래와 같이 기존 User 엔티티 외의 Photo 라는 Foreign Table 을 만들어 관계를 설정하도록 하겠다.

src/user/domain/Photo.ts

import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm/index';
import { User } from './User';
@Entity()
export class Photo {
@PrimaryGeneratedColumn()
id: number;
@Column()
url: string;
@ManyToOne(type => User, user => user.photos)
@JoinColumn({ name: 'ref_userId' }
user: User;
}

src/user/domain/User.ts

import { Column, Entity, OneToMany, PrimaryColumn, PrimaryGeneratedColumn } from 'typeorm/index';
import { Photo } from './Photo';
@Entity()
export class User {
@PrimaryColumn()
userId: string;
@Column()
userName: string;
@Column()
userPassword: string;
@Column()
age: number;
@Column({ default: true })
isActive: boolean;
@OneToMany(type => Photo, photo => photo.user)
photos: Photo[]
}

User 와 Photo 의 관계를 설정하는데 있어서 User 는 Photo 와 1:N 의 관계를 맺는다고 했을 때 위와 같이 설정하면 된다. 그리고 TypeORM 모듈 설정이 Sync 가 true 되어 있다면 Context Reload 시 아래와 같이 자동으로 테이블이 생성이 되고 제약 설정이 자동으로 된다. ERD 를 확인하면 아래와 같다.

photo 에서 ref_userId 컬럼이 Foreign Key 로 설정되어있고 이 Foreign Key는 User 테이블의 userId Primary Key 연결되어 있다.

이 설정을 하는데 있어서 app.module.ts 의 TypeORMModule 설정이 아래와 같이 Photo 가 Entities 에 추가가 되어야 에러가 나지 않는다.

... 생략 ...
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'uaa',
// autoLoadEntities: true,
**entities: [User, Photo],**
synchronize: true,
}),
... 생략 ...

수동적으로 추가를 할 수 있는 반면 자동으로 Entity가 Load가 되게 하려면 autoLoadEntities 설정이 true 로 되어 있어야 한다. 물론 사용하는 모듈에서는 아래와 같이 TypeOrmModule.forFeature 함수에선 해당 Entity를 사용하겠다 라고 선언 해줘야 한다.

src/user/user.module.ts

import { Module } from '@nestjs/common';
import { UserController } from './user.controller';
import { UserService } from './user.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { User } from './domain/User';
import { Photo } from './domain/Photo';
@Module({
imports: [TypeOrmModule.forFeature([User, Photo])],
controllers: [UserController],
providers: [UserService],
})
export class UserModule {}

Transaction 처리

데이터베이스의 Transaction 처리는 비즈니스 로직상 굉장히 중요한 기능이다. 예를 들어 서로 다른 트랜잭션들을 처리하는 도중 하나의 단위 트랜잭션에서 에러가 발생한다면 이전에 성공 했던 트랜잭션들을 다시 rollback 을 해야 데이터의 정합성이 깨지지 않는다. 우선 TypeORM 을 논하기 전에 데이터베이스에서 서로 다른 트랜잭션을 하나로 처리하는 과정은 다음과 같은 Flow 가 존재할 것이다.

[DB 트랜잭션 Flow]

  1. autoCommit = false 로 설정함
  2. 서로 다른 트랜잭션을 부분적으로 처리한다.
  3. 만약, 모든 트랜잭션이 정상적으로 완료되었 을 때는 Commit 을 한다.
  4. 만약, 트랜잭션 중 하나의 트랜잭션이 비정상적으로 처리되면 Rollback 을 수행한다.

위의 데이터베이스 상에 처리 과정을 숙지하고 TypeORM 을 통해 어떻게 트랜잭션을 처리 하는 지에 알아보도록 하자. 일단 여러가지 방법이 존재한다. 데코레이터로 @Transactional 이라는 선언을 하여 해당 메서드 위에 간편하게 처리하는 방법이 있는가 하면 Callback Style 로 처리하는 방법들이 존재한다. 하지만 Nest 재단에서 느낌상으로 추천하고 권고 하는 방법은 아래 방식인 거 같다. 명시적으로 트랜잭션을 코드 상에서 처리 하는 방법이 좋은 방법이라 생각 하는 것으로 추측된다.

명시적으로 queryRunner 를 통해 트랜잭션을 처리하는 방법은 아래와 같다. 우선 Connection 객체를 의존성 주입한다. 이 의존성은 이미 TypeORM 다이내믹 모듈을 import 한 것만으로도 의존성을 가져올 수 있다. 이 의존성을 토대로 createQueryRunner() 함수를 통해 queryRunner 객체를 가져올 수 있고 이 객체를 통해 트랜잭션 매니저를 수행할 수 있다. 코드를 살펴보자.

src/user/user.service.ts

... 생략 ...
constructor(
@InjectRepository(User) private userRepository: Repository<User>,
private connection: Connection
) {
this.connection = connection;
this.userRepository = userRepository;
}
... 생략 ...
/**
* 다수의 유저 입력
*/
async createUsers(users: User[]) {
let isSuccess = true;
const queryRunner = this.connection.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
**await queryRunner.manager.save(users[0]); // (1)
await queryRunner.manager.save(users[1]); // (2)**
await queryRunner.commitTransaction();
} catch (err) {
console.log('Rollback 실행..')
await queryRunner.rollbackTransaction();
isSuccess = false;
} finally {
await queryRunner.release();
return isSuccess;
}
}

우선 테스트이기 때문에 간단히 처리 했다. 의도하는 바는 User 엔티티 타입의 객체를 Array 타입으로 가져와서 index 0, 1 번째만 각각 서로 다른 트랜잭션으로 저장할 수 있도록 코드를 (1), (2) 작성하였다.

이제에는 테스트 케이스를 만들어 아래와 같이 성공과 실패 두 가지 경우를 살펴 보도록 하겠다.

Jest 테스트

export const getTypeOrmModule = () => {
return TypeOrmModule.forRoot({
type: 'postgres',
host: 'localhost',
port: 5432,
username: 'postgres',
password: 'password',
database: 'uaa',
autoLoadEntities: true,
// entities: [User, Photo],
synchronize: true,
})
};
describe('테스트', () => {
let app: TestingModule;

beforeAll(async () => {
app = await Test.createTestingModule({
imports: [getTypeOrmModule(), TypeOrmModule.forFeature([User, Photo])],
controllers: [UserController],
providers: [UserService, TestService],
}).compile();
});
describe('UserService 테스트', () => {
it('**createUsers 트랜잭션 처리 테스트 - 성공',** async () => {
const userService = app.get<UserService>(UserService);
const isSuccess = await userService.createUsers([
new User('a1', 'a1', '1234', 10, true, []),
new User('b1', 'b1', '1234', 10, true, [])
]);
console.log(isSuccess);
expect(isSuccess).toBeTruthy();
});
it('**createUsers 트랜잭션 처리 테스트 - 실패'**, async () => {
const userService = app.get<UserService>(UserService);
const isSuccess = await userService.createUsers([
new User('c1', 'c1', '1234', 10, true, []),
new User(null, 'd1', '1234', 10, true, [])
]);
console.log(isSuccess);
expect(isSuccess).toBeFalsy();
});
});
});

테스트 케이스의 사전 준비를 위해 TestingModule 을 통해 사용하게 될 모듈을 불러오고 TypeORM 셋팅을 beforeAll 로 단위 테스트를 하기 전에 셋팅하였다. 그 이후 ‘UserService 테스트’ 를 수행하게 되는 데 첫 번째 시나리오는 성공의 케이스다.

두 개의 User 엔티티에 값을 셋팅하고 저장해보았다 성공 시 boolean 으로 true 를 내뱉게 될 것이다.

실행을 해보면 아래와 같다.

테스트 성공 시 ( true로 나오는 것으로 확인 됨 )

User 테이블에 데이터를 확인하면 a1, b1 의 userId 가 기록 된 것을 볼 수 있다.

테스트 실패 시 ( false 로 나오는 것으로 확인 됨 )

User 테이블에 데이터를 확인하면 부분적으로 성공 케이스 였던 C1 유저를 저장하는 트랜잭션도 데이터가 없는 것을 확인 할 수 있다. ( Rollback 이 된 것이다 )

이번 시간에는 Entity 간 Relation 설정과 Transaction 처리 방법을 알아보았다.

다음. [NestJS] TypeORM Subscribers 기능

--

--