Typeorm 0.3 Migration

TypeORM 0.2.X에서 0.3.X 버전으로 업데이트하면서 변경된 사항과 NestJS에서 CustomRepository를 사용하는 방법을 정리했습니다.

블로그를 처음에 개발해놓고, 오랜 시간동안 방치해놓았다. 이직한 이후에 블로그 최신화를 진행하지 않아서, 블로그에서 사용하고 있는 여러 라이브러리들의 버전이 업데이트 되어있었다. 그동안 숙제처럼 생각하고 있었던 라이브러리 버전 업데이트를 차차 진행하고자 한다. 가장 시급도가 높은 업데이트였던 Typeorm 버전 업데이트를 진행하였다. Typeorm 은 아직 메이저 버전이 나오지 않았는데, 0.2.X 버전에서 0.3.X 버전으로 업데이트되면서 다양한 사용법이 변경되었다. 그 중에서는 Nestjs 와 같이 사용하면서, 사용법이 변경된 부분도 있어서 Typeorm 버전 업 시 유의 할 사항을 정리해 보았다.

// 0.2
return createConnection({
    return new DataSource({
      type: 'postgres',
      host: HOST,
      username: USERNAME,
      entities: entities,
});

// 0.3
const dataSource = new DataSource({
  type: 'postgres',
  host: HOST,
  username: USERNAME,
  entities: entities,
});
return dataSource.initialize();


// user.repository.ts
export const UserRepository = dataSource.getRepository(User).extend({
    findByName(firstName: string, lastName: string) {
        return this.createQueryBuilder("user")
            .where("user.firstName = :firstName", { firstName })
            .andWhere("user.lastName = :lastName", { lastName })
            .getMany()
    },
})

// user.controller.ts
export class UserController {
    users() {
        return UserRepository.findByName("Timber", "Saw")
    }
}


위의 예시에서 바로 알 수 있지만, 가장 큰 특징은 class 형태가 아니라는 것이다. nestjs 에서는 DynamicModule 형태로 Inject 할 수 있지만, Class 형태로 사용하고 싶다면, Custom Decorator 을 만들어서 사용하는 것이 편하다.

// custom-repository.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const TYPEORM_CUSTOM_REPOSITORY = Symbol('TYPEORM_CUSTOM_REPOSITORY');

// eslint-disable-next-line @typescript-eslint/ban-types
export function CustomRepository(entity: Function): ClassDecorator {
  return SetMetadata(TYPEORM_CUSTOM_REPOSITORY, entity);
}

// example
@CustomRepository(User)
export class UserCustomRepository {

}

먼저, 특정 클래스가 Custom Repository 인지 기록할 @CustomRepository(Entity) 데코레이터가 필요하다. 해당 데코레이터는, 코드 평가 시에, 어떤 Entity 에 대한 CustomRepository 인지 표기할 수 있다.

import { Global, Module } from '@nestjs/common';
import { postgresConnectionFactory } from './database-config.factory';
import { DB } from '../../constant';
import { TYPEORM_CUSTOM_REPOSITORY } from '../../decorators/custom-repository.decorator';
import { DataSource } from 'typeorm';

@Global()
@Module({})
export class DatabaseConfigModule {
  private static _customRepositories = new Map<string, any>();

  static forRoot() {
    return {
      module: DatabaseConfigModule,
      providers: [postgresConnectionFactory],
      exports: [DB.PROVIDER.POSTGRESQL_TYPEORM],
    };
  }

  static getCustomRepositoryProvider<
    CustomRepository extends new (...args: any[]) => any
  >(
    customRepositories: CustomRepository[],
    dataSource: DB.PROVIDER = DB.PROVIDER.POSTGRESQL_TYPEORM,
  ) {
    return customRepositories.map((repository) => {
      const entity = Reflect.getMetadata(TYPEORM_CUSTOM_REPOSITORY, repository);

      if (!entity) {
        throw new Error('Entity Metadata 가 존재하지 않습니다');
      }

      return {
        inject: [dataSource],
        provide: repository,
        useFactory: (dataSource: DataSource): typeof repository => {
          const baseRepository = dataSource.getRepository(entity);
          const provider =
            this._customRepositories.get(entity.name) ??
            new repository(
              baseRepository.target,
              baseRepository.manager,
              baseRepository.queryRunner,
            );
          this._customRepositories.set(entity.name, provider);

          return provider;
        },
      };
    });
  }
}

다음으로, 모듈에 CustomRepository 를 등록할 때 사용할 수 있는 getCustomRepositoryProvider 메소드를 생성하였다. 해당 메소드는 CustomRepository 에 대한 메타데이터를 읽어들인 후, 해당 Repository 를 Entity 의 기본 Repository 와 비슷하게 생성해준다.

import { Between, In } from 'typeorm';
import { DefaultRepository } from '../common/repository/default.repository';
import { Post } from '../entities/Post';
import { GetPostDto } from '../dto/GetPost.dto';
import { CustomRepository } from '../decorators/custom-repository.decorator';

@CustomRepository(Post)
export class PostRepository extends DefaultRepository<Post> {

}

위와 같은 CustomRepository 를 선언해 주면, 해당 클래스의 생성자를 기본 Repository (datasource.getRepository(Post)) 의 생성방식과 동일하게 생성해주면 된다.

위의 과정을 활용해서, module 에서는 다음과 같이 등록하면 된다.

import { Module } from '@nestjs/common';
import { AuthModule } from '../auth/auth.module';
import { PostController } from './post.controller';
import { PostService } from './post.service';
import { DirectoryModule } from '../directory/directory.module';
import { PostRepository } from './post.repository';
import { TagRepository } from '../tag/tag.repository';
import { TagPostRepository } from '../tag/tag-post.repository';
import { DatabaseConfigModule } from '../config/database/database-config.module';

@Module({
  imports: [AuthModule, DirectoryModule],
  controllers: [PostController],
  providers: [
    PostService,
    ...DatabaseConfigModule.getCustomRepositoryProvider([
      PostRepository,
      TagRepository,
      TagPostRepository,
    ]),
  ],
  exports: [PostService],
})
export class PostModule {}

이것도 읽어보세요