최근 TypeORM의 버전이 0.3.x로 올라가면서 많은 변경이 생겼다. 그 중 큰변화는 기존에 있던 레퍼지토리가 변경된 것이라고 생각합니다.
import {EntityRepository, Repository} from "typeorm";
import {User} from "../entity/User";
@EntityRepository(User)
export class UserRepository extends Repository<User> {}
@Module({
imports:[TypeOrmModule.forFeature([User,UserRepository]),
providers:[UserService],
})
기존에는 위와 같이 커스텀 레퍼지토리 클래스를 만들어 개발할 수 있었지만, @EntityRepository()
데코레이터가 depreceated되면서 사용이 위와 같이 사용할 수 없어졌다.
// 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")
}
}
공식문서에 따르면 위와 같이 커스텀 레퍼지터리를 사용하라고 하지만 NestJS에서는 보통 TypeOrmModule이라는 @nestjs/typeorm
패키지를 사용하여 dataSource
를 저렇게 쉽게 가져올 수 없다. 또한 provider와 IoC컨테이너를 활용하는 Nest 프레임워크에 적합하지 않은 방법이라는 생각도 들었다.
간단하게 생각하고 찾은 방법은 3가지 정도이고, 어떤게 좋은 방법인지는 확실하게는 모르겠다.
provider는 Nest에 있는 기본 개념으로 의존성 주입(DI)될 수 있는 것들을 의미하며, Nest의 런타임 횐경속에서 각각의 provider들은 적절하게 주입된다.
Nest에서 의존성을 적절히 주입해주기 위해선 @Injectable()
데코레이터를 통해 Nest의 IoC컨테이너에 등록해주어야한다. 아래와 같이 주입되기 원하는 클래스위에 @Injectable()
데코레이터를 선언해줌으로서 UserService는 Nest의 IoC컨테이너에 등록된다.
@Injectable()
export class UserService {
}
이렇게 생성된 유저 서비스는 다른 클래스에 주입되어 사용할 수 있게 된다.
export class UserController {
constructor(private readonly userService:UserService){};
}
이 때 기본적으로 UserService
는 싱글톤 인스턴스로 언제 어디서 주입하던 같은 인스턴스가 주입되게 된다.
이를 scope
라고하는데 DEFAULT, REQUEST, TRANSIENT가 있다.
Scope는 성능적 문제를 일으킬 수 있고 계층적 구조를 가지기 때문에 적절히 사용해야 한다.
그렇다면 어떻게 커스텀 레퍼지토리를 만들고 사용해야 할까?
간단하게 다른 provider들과 똑같이 사용하는 방법이 있다.
// user.repository.ts
@Injectable()
export class UserRepository extends Repository<User> {
constructor(private readonly dataSource: DataSource){
const baseRepository = dataSource.getRepository(User);
super(baseRepository.target, baseRepository.manager, baseRepository.queryRunner);
}
}
// user.module.ts
@Module({
providers: [UserRepository]
})
export class UserModule {}
Repository<T>
는 TypeORM에서 사용되는 repository클래스로 아래와 같은 생성자를 통해 인스턴스화 된다. 따라서 dataSource를 injection받아 적절하게 값을 넣어준 방법이다.
constructor(target: EntityTarget<Entity>, manager: EntityManager, queryRunner?: QueryRunner);
위의 방법을 사용해 @CustomRepository()
데코레이터를 사용하는 방법이다. 0.2.x 버전에서 사용되던 @EntityRepository()
데코레이터와 방법이 유사하며 0.2.x에서 0.3.x버전으로 올릴 경우 사용하면 좋을 것 같다.
우선 아래와 같이 CustomRepository 데코레이터를 정의해준다. SetMetadata
는 어딘가에서 사용될 수 있는 메타데이터를 설정하는 것을 의미한다. Key-Value 방식으로 저장된다.
import { SetMetadata } from '@nestjs/common';
export const TYPEORM_EX_CUSTOM_REPOSITORY = 'TYPEORM_EX_CUSTOM_REPOSITORY';
export function CustomRepository(entity: Function): ClassDecorator {
return SetMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, entity);
}
export declare const SetMetadata: <K = string, V = any>(metadataKey: K, metadataValue: V) => CustomDecorator<K>;
SetMetadata
에 의해 저장된 정보는 Reflector
클래스를 통해 접근할 수 있다. 이를 통해 아래와 같이 동적으로 모듈을 생성하는 TypeORMCustomModule
을 정의해준다.
import { DynamicModule, Provider } from '@nestjs/common';
import { getDataSourceToken } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { TYPEORM_EX_CUSTOM_REPOSITORY } from './decorators/custom-repository.decorator';
export class TypeOrmCustomModule {
// CustomeRepository를 입력받음
public static forCustomRepository<T extends new (...args: any[]) => any>(...repositories: T[]): DynamicModule {
const providers: Provider[] = [];
for (const repository of repositories) {
// 각각의 커스텀레퍼지토리에 TYPEORM_EX_CUSTOM_REPOSITORY Key로 정의된 메타데이터를 가져온다
const entity = Reflect.getMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, repository);
if (!entity) {
continue;
}
// dataSource를 주입받아 방법 1과 같이 커스텀 레퍼지토리를 만들어주고 provider에 추가한다.
providers.push({
inject: [getDataSourceToken()],
provide: repository,
useFactory: (dataSource: DataSource): typeof repository => {
const baseRepository = dataSource.getRepository<any>(entity);
return new repository(baseRepository.target, baseRepository.manager, baseRepository.queryRunner);
},
});
}
return {
exports: providers,
module: TypeOrmCustomModule,
providers,
};
}
}
TypeORM 0.2.x 버전과 유사하게 사용가능하다.
// user.repository.ts
@CustomRepository(User)
export class UserRepository extends Repository<User> {}
// user.module.ts
@Module({
imports: [TypeOrmCustomModule.forCustomRepository(UserRepository)]
})
export class UserModule {}
TypeORM 문서에서 볼 수 있는 Custom Repository를 생성하는 방법을 사용한다. provider를 추가하는 방법에는 여러가지가 있는데, 그 중 Factory를 사용하는 방법이다. 이를 위해선 필수적으로 서비스에서 UserRepository 타입을 선언하기 위해 interface
를 정의해주어야 한다.
// user.repository.ts
export interface UserRepository extends Repository<User> {
getById(id: number): Promise<Board>;
}
export const UserRepositoryFactory = (dataSource: DataSource) =>
// TypeORM문서에서 Custom Repository를 만드는 방법
dataSource.getRepository(User).extend({
getById(id: number) {
return this.findOneBy({ id });
},
});
// user.module.ts
@Module({
providers: [
{
provide: 'UserRepository',
useFactory: UserRepositoryFactory,
inject: [getDataSourceToken()],
},
],
exports:['UserRepository']
})
대신 이 방법은 사용시 아래와 같이 @Inject('UserRepository')
를 추가해주어야 하는 불편함이 있다. 또한 repository를 exports하기 위해선 위와 같이 String을 exports하고 import한 모듈 내에서 아래와 마찬가지로 사용할 수 있다.
// user.service.ts
@Injectable()
export class UserService {
constructor(@Inject('UserRepository') private readonly userRepository: UserRepository){}
}
reference