typeorm 0.3.x 에서는 기존에 사용하던 @EntityRepository가 deprecated 되었다.
그 말인 즉슨 커스텀 레포지토리 패턴을 사용할 수 없게되었다.
눈물이 난다.. 눈물이 나 하지만 의지의 한국인은 방법을 찾아낸다. 그 해결과정을 공유해보려고 한다~
스택이 터졌어요 짱!!
근데 커스텀 레포를 이해하려면 레포 패턴부터 알아야한다. 레포 패턴은 다들 아실거라 생각하고 설명은 따로 안하려고 합니다!!
간단하게 설명하면 db접근을 repo에서 하고 service는 비즈니스 로직에 집중하자!! 패턴입니다.
자 이제 드가봅시다.
// typeorm-ex.decorator.ts
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);
}
첫번째는 바로 데코레이터부터 만들어주는 것입니다.
이 데코레이터가 바로 @EntityRepository가 될 아이입니다.
SetMetadata는 key: value형태입니다. 즉 key가 TYPEORM_EX_CUSTOM_REPOSITORY되고 value가
entity가 됩니다.
여기서 어 근데 메타데이터는 왜 쓰는거에요?? 라는 질문이 나올 수 있기 때문에 짚고 넘어가겠습니다.
Nest에서 DI는 런타임에서 제어합니다. 그렇기 때문에 런타임전에 메타데이터 세팅을 해놓고 런타임에 다이나믹모듈 방식으로 DI를 해줄 것이기 때문입니다. 다이나믹 모듈이 뭐냐고요!? 그건 다이나믹 모듈을 보시기 바랍니다.
Nest가 어떻게 DI를 하는지 모르겠다구요? 친절하게 이것까지 알려드리겠습니다. 더 이상 질문은 받지않겠습니다.
총 3가지의 step을 거치게 됩니다.
1. 데코레이터가 등록된 서비스 클래스를 NestIoc 컨테이너에서 관리할 수 있도록 선언합니다.
2. Controller는 생성자 주입이 있는 Service 토큰에 대한 종속성을 선언합니다.
3. appModule에서 토큰 service를 클래스 Service와 연결합니다. (class 부를때 import하고 부르자나요 그거 말하는 겁니다.
import { ExService } from ex.service.ts
@Module({
providers: [ExService],
})
이래도 이해가 잘 안가신다면 Nest 요것을 보는것을 추천드립니다.
꿀정보: 종속성 순서는 Nest가 부트스트랩 과정중에 종속성 그래프를 사용해 bottom-up 방식으로 해결합니다.
// typeorm-ex.module.ts
import { DynamicModule, Provider } from "@nestjs/common";
import { getDataSourceToken } from "@nestjs/typeorm";
import { DataSource } from "typeorm";
import { TYPEORM_EX_CUSTOM_REPOSITORY } from "./typeorm-ex.decorator";
export class TypeOrmExModule {
public static forCustomRepository<T extends new (...args: any[]) => any>(repositories: T[]): DynamicModule {
const providers: Provider[] = [];
for (const repository of repositories) {
const entity = Reflect.getMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, repository);
if (!entity) {
continue;
}
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: TypeOrmExModule,
providers,
};
}
}
코드만 봐도 뭐가 뭔지 모르겠죠? 저도 몰라서 공부하고 알려드리는 겁니다.
다이나믹 모듈에 대해서 먼저 알아야하니까 위에 링크걸어놓은거 보고 오세요!
그냥 천천히 코드를 읽어나가는 식으로 설명하겠습니다. 집중집중!!
데코레이터 만드는 패턴은
function classDecorator<T extends { new (...args: any[]): {}>(constructor:T){}
이건 타입스크립트에 나와있으니 찾아보시면 됩니다.
자자!! 지금부터 진짜 집중!!
위의 패턴을 보면 한 가지 사실을 알 수 있습니다. 데코레이터는 매개변수로 생성자를 포함하는 객체가 필요하다는 사실을 알 수 있습니다. 이 개체는 다른 클래스를 인스턴스화할 수 있으므로 함수를 사용해야 합니다.
그럼 이제 우리 코드로 살펴보겠습니다!!
public static forCustomRepository<T extends new (...args: any[]) => any>(repositories: T[]): DynamicModule
new는 결국 클래스 데코레이터의 유형 클래스를 제한하기 위해 extends다음에 지정합니다. 결국 new의 뜻은 아 진짜 제발 생성자좀!!그리고 바로 뒤에 (...args: any[])는 나는 어떤 녀석이건 상관하지 않고 최선을 다하지!!이 말인 즉슨 모든 유형의 다양한 인수를 사용하는 생성자가 있는 일부 개체를 말합니다.
그냥 말보다 코드로 보여드리겠습니다.
function f(...args: any[])
f("a",1, "csd")
뭐 대충 이런 느낌입니다. 다 받아들이는 녀석이죠. 마지막으로 생성자로는 repo생성자 주는데 배열 타입 주세용~ 뭐 이런 느낌적인 느낌입니다.
나한테 뭐 보낼땐 repository 배열 타입으로 보내라!! 이렇게 이해 하면 됩니다.
다음 코드를 살펴볼까요?
for (const repository of repositories) {
const entity = Reflect.getMetadata(TYPEORM_EX_CUSTOM_REPOSITORY, repository);
if (!entity) {
continue;
}
받은 repo들을 for문을 돌면서 아까 setMetadata했으니까 이번엔 get으로 얻어옵니다.
Reflect는 인터셉터 느낌으로 보면 됩니다. 도중에 샤샥 훔쳐오는 느낌으로 생각하면 되요
그리고 이제 제일 중요한 부분!!
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);
},
});
providers 공급자들을 채워줘야합니다.
inject로 getDataSourceToken()으로 DB데이터 연결을 얻습니다!!
그 다음 provide로는 repository를 제공할 것입니다.
useFactory 난, 동적으로 결정할거야!! 하는 녀석입니다. 이녀석도 설명하자면 꽤나 긴데 그래도 해드리겠습니다.
대충 개념부터 살펴보자면,
공급자를 동적으로 생성가능한 녀석!
실제 공급자는 Factory function에서 반환되는 녀석
다른 공급자에 의존하는 복잡한 녀석이 될수도 있고(complex Factory)
홀로서기가 가능한 단순한 녀석이 될 수도 있습니다.(simple Factory)
inject property는 Nest가 해결하고 인스턴스화 합니다. 프로세스 중 Factory function에 인수로 전달할 공급자 배열을 받습니다. Nest는 동일 순서로 주입 목록의 인스턴스를 인수로 Factory function에 전달합니다.
이해 안되면 Nest Factory 읽어보시면 됩니다.
결국 그래서 마지막에 return new repository보이시죠? 생성자 반환합니다!!
나머지는 이해가 어려운 부분이 없으므로 여기까지 하겠습니다.
기존에 @EntityRepositoy 지우고 @CustomRepository로 대체하고 module에서 사용하던 Typeorm.forFeature 지우고 TypeOrmExModule.forFeature([forCustomRepository])로 사용하시면 이제 0.2.x 버전처럼 동작하는 모습을 보실 수 있습니다!
출저: 스택이 터졌어요
typeorm 0.3
안녕하세요 혹시 Custom Repository 적용하고 e2e 테스트 해보셨나요? 제가 시도해봤는데 이런 에러가 뜨더라구요ㅠㅠ
Nest can't resolve dependencies of the UserRepository (?). Please make sure that the argument DataSource at index [0] is available in the TypeOrmExModule context.
그래서 DataSource를 provider에 추가하면 또 이런 에러가 뜨고요
TypeError: Cannot read properties of undefined (reading 'name')
마지막으로
테스팅 모듈에
providers: [
{
provide: DataSource,
useClass: class MockDataSource {},
},
]
이런 코드를 추가해서 mocking을 해보려고 했으나 그래도 첫번째 오류가 뜹니다.
혹시 해결방법 아실까 해서 댓글 달아봅니다. 감사합니다.
안녕하세요 ! 버전 업데이트 이후 엄청 헤매고 있었는데, 좋은 글 감사합니다 :)
혹시 마지막 문장에 TypeOrmModule.forFeature을 지우고 TypeOrmExModule.forCustomRepository로 대체하라고 하셨는데, 저는 TypeOrmModule.forFeature을 지우니까
imports를 해주라는 에러가 발생해서 지우지 않고 그대로 둔 상태에서 추가하는 형식으로 해결했는데 혹시 어떤 문제일까요?