Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@Transactional() 및 TypeORM 설정 #1

Open
jintak0401 opened this issue Mar 3, 2023 · 0 comments
Open

@Transactional() 및 TypeORM 설정 #1

jintak0401 opened this issue Mar 3, 2023 · 0 comments

Comments

@jintak0401
Copy link
Contributor

jintak0401 commented Mar 3, 2023

image

TypeORM 트랜잭션

Transaction을 위한 키값

// utils/constants.ts

export const PLANDAR_NAMESPACE = 'plandar';
export const PLANDAR_ENTITY_MANAGER = 'plandarEntityManager';

namespace와 관련된 key값들

  • PLANDAR_NAMESPACE
  • PLANDAR_ENTITY_MANAGER

TransactionMiddleware

매 요청 전에 수행될 미들웨어

// common/middleware/transaction.middleware.ts

import { Injectable, NestMiddleware } from '@nestjs/common';
import { createNamespace, getNamespace, Namespace } from 'cls-hooked';
import { NextFunction } from 'express';
import { EntityManager } from 'typeorm';

import { PLANDAR_ENTITY_MANAGER, PLANDAR_NAMESPACE } from '@/utils/constants';

@Injectable()
export class TransactionMiddleware implements NestMiddleware {
  constructor(private readonly em: EntityManager) {}
  use(_req: Request, _res: Response, next: NextFunction) {
    const namespace =
      getNamespace(PLANDAR_NAMESPACE) ?? createNamespace(PLANDAR_NAMESPACE); // 1
    return namespace.runAndReturn(async () => {
      Promise.resolve()
        .then(() => this.setEntityManager()) // 2
        .then(next);
    });
  }

  private setEntityManager() {
    const namespace = getNamespace(PLANDAR_NAMESPACE) as Namespace; 
    namespace.set<EntityManager>(PLANDAR_ENTITY_MANAGER, this.em); 
  }
}
  1. namespace가 존재하지 않는다면 만들어주며, 존재한다면 기존의 namespace를 가져온다.
  2. 주입받은 EntityManager인 em을 현재 namespace.active.plandarEntityManager 로 설정해준다.

TransactionManager

이 클래스의 getEntityManager()를 통해 EntityManager를 가져온다.

// common/transaction.manager.ts

import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { createNamespace, getNamespace } from 'cls-hooked';
import { EntityManager } from 'typeorm';

import { PLANDAR_ENTITY_MANAGER, PLANDAR_NAMESPACE } from '@/utils/constants';

@Injectable()
export class TransactionManager {
  getEntityManager(): EntityManager {
    const nameSpace =
      getNamespace(PLANDAR_NAMESPACE) ?? createNamespace(PLANDAR_NAMESPACE);
    if (!nameSpace)
      throw new InternalServerErrorException(
        `${PLANDAR_NAMESPACE} is not active`,
      );
    return nameSpace.get(PLANDAR_ENTITY_MANAGER);
  }
}

TransactionManager 객체인 txManager 에 대해, txManager.getEntityManager() 를 통해 미들웨어에서 설정해준 entityManager를 가져온다.

@transactional()

service의 함수에 이 데코레이터를 사용하면 해당 함수를 트랜잭션으로 묶이게 해준다.

// common/decorator/transaction.decorator.ts

import { InternalServerErrorException } from '@nestjs/common';
import { getNamespace } from 'cls-hooked';
import { EntityManager } from 'typeorm';

import { PLANDAR_ENTITY_MANAGER, PLANDAR_NAMESPACE } from '@/utils/constants';

export function Transactional() {
  return function (
    _target: object,
    _propertyKey: string | symbol,
    descriptor: TypedPropertyDescriptor<any>,
  ) {
    const originMethod = descriptor.value;

    async function transactionWrapped(...args: unknown[]) {
      const nameSpace = getNamespace(PLANDAR_NAMESPACE);
      if (!nameSpace || !nameSpace.active)
        throw new InternalServerErrorException(
          `${PLANDAR_NAMESPACE} is not active`,
        );

      const em = nameSpace.get(PLANDAR_ENTITY_MANAGER) as EntityManager;
      if (!em)
        throw new InternalServerErrorException(
          `Could not find EntityManager in ${PLANDAR_NAMESPACE} nameSpace`,
        );

      return await em.transaction(async (tx: EntityManager) => {
        nameSpace.set<EntityManager>(PLANDAR_ENTITY_MANAGER, tx); // 1
        return await originMethod.apply(this, args); // 2
      });
    }

    descriptor.value = transactionWrapped;
  };
}
  1. namespace.active.plandarEntityManagertx를 할당해준다.
  2. 트랜잭션 안(em.transaction의 callback 함수)에서 service 함수를 수행해준다.

RootRepository

각 Repository를 구현할 때 이 RootRepository를 상속해주어야 한다.

// common/root.repository.ts

import { Inject } from '@nestjs/common';
import { EntityTarget, Repository } from 'typeorm';

import { TransactionManager } from '@/common/transaction.manager';

export abstract class RootRepository<T> extends Repository<T> { // 1
  constructor(
    @Inject(TransactionManager) private readonly txManager: TransactionManager,
  ) {
    super(RootRepository, txManager.getEntityManager());  // 2
  }

  abstract getName(): EntityTarget<T>;  // 3

  protected getRepo(): Repository<T> { // 4
    return this.txManager.getEntityManager().getRepository(this.getName());
  }
}
  1. Repository를 상속받아 Service에서도 각 Repository를 이용해 TypeORM의 메소드들을 사용할 수 있게 해준다.
  2. 1번의 상속을 위해 super를 해준다.
  3. 각 Repository에서 구현해주어야 한다.
  4. 각 Repository에서 this.getRepo()를 통해 쿼리를 수행할 수 있다.

RootRepository를 상속받은 UserRepository

// api/user/user.repository.ts

export class UserRepository extends RootRepository<User> {  // 1
  getName(): EntityTarget<User> { // 2
    return User.name;
  }
}
  1. 각 Repository는 RootRepository<Entity 이름> 을 해주어야 한다.
  2. 각 Repository는 getName() 함수를 위와 같이 구현해주어야 한다.

UserService

아래처럼 TypeORM의 함수들을 사용할 수도 있으며, @Transactional()을 이용해 트랜잭션으로 함수를 관리할 수 있다.

// api/user/user.service.ts

import { Injectable } from '@nestjs/common';

import { PlanRepository } from '@/api/plan/plan.repository';
import { UserRepository } from '@/api/user/user.repository';

@Injectable()
export class UserService {
	constructor(
    private readonly userRepo: UserRepository,
    private readonly planRepo: PlanRepository,
  ) {}

  async getUsers() {
    return this.userRepo.find(); // 1
  }

@Transactional() // 2
  async createUser() {
    return await Promise.all([
      this.userRepo.createUser('user'),
      this.planRepo.createPlan('plan'),
    ]);
  }
}
  1. RootRepository 에서 TypeORM의 Repository를 상속했기 때문에 UserRepository에서 구현하지 않은 find()` 함수를 사용할 수 있다.
  2. @Transactional() 을 통해 해당 service 함수를 transaction으로 만들어준다.
    • 만약 @Transactional()에서 다른 @Transactional() 을 호출한다면, 후자의 @Transactional() 은 세이브포인트로 동작한다.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant