저번에는 실제 디비를 연결하지 않고, 고정 값으로 dataloader 를 구현해봤다면, 이번에는 실제로 디비에 연결해서 dataloader 가 어떻게 돌아가는지 확인해보자.
전체적인 플로우를 먼저 살펴보자
시나리오는 다음과 같다.
" 전체 채팅방에 대해서 각 채팅방에 속해있는 유저들의 정보를 긁어온다. "
chat resolver → chat service → chat repository → user resolver
@Resolver(() => RoomModel)
// 이 resolver 는 chatModel 을 뽑아내기 위한 리졸버이다
export class ChatResolver {
constructor(
private readonly chatService: ChatService,
private readonly chatLoader: ChatLoader,
) {}
@Query(() => [UserModel], { name: 'user' })
init() {}
/**
*
* @description 전체 채팅방과 그 안에 포함된 유저들의 정보를 불러오자
*/
@Query(() => [RoomModel])
async getAllRoomInfo(): Promise<RoomModel[]> {
return await this.chatService.getAllRoomIds();
}
/**
* @description user resolver 로 필요한 정보를 얻기 위한 쿼리 전송
* roomId 로 그 안에 속해있는 유저들의 정보를 긁어와야 하는데,
* resolveField 호출은 여러번 해도 실제 디비에서 조회하는 로직은 한번만 할 수 있도록
*/
@ResolveField('users', () => [UserModel])
async getUsers(@Parent() room: RoomModel): Promise<UserModel[]> {
try {
const result = await this.chatLoader.findByUserId.load(room.roomId);
return result;
} catch (error) {
this.chatLoader.findByUserId.clear(room.roomId);
}
}
}
@Injectable()
export class ChatService {
constructor(private readonly roomRepo: RoomRepository) {}
findAll() {
return `This action returns all chat`;
}
async getAllRoomIds(): Promise<RoomModel[]> {
const rooms = await this.roomRepo.getAllRoomIds();
const result: RoomModel[] = [];
for (const r of rooms) {
result.push({
roomId: r.id.toString(),
});
}
return result;
}
}
@Injectable()
export class RoomRepository extends Repository<RoomEntity> {
constructor(private dataSource: DataSource) {
super(RoomEntity, dataSource.createEntityManager());
}
async getAllRoomIds(): Promise<RoomEntity[]> {
return await this.createQueryBuilder().getMany();
}
}
@Resolver(() => UserModel)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query(() => [UserModel], { name: 'user' })
init() {}
/**
*
* @description chat resolver 에서 처리하지 못하는 쿼리를 여기서 처리함 (find user)
* resolverField 를 여기서 수신해서 처리한다.
*/
@ResolveReference()
async resolveReference(reference: {
__typename: string;
id: string;
}): Promise<UserModel> {
return await this.userService.getUser(reference.id);
}
}
@Injectable()
export class UserService {
constructor(private readonly userRepo: UserRepository) {}
async getUser(userId: string): Promise<UserModel> {
const user = await this.userRepo.getUser(userId);
const result: UserModel = {
id: user.id,
name: user.name,
};
return result;
}
}
@Injectable()
export class UserRepository extends Repository<UserEntity> {
constructor(private dataSource: DataSource) {
super(UserEntity, dataSource.createEntityManager());
}
async getUser(userId: string): Promise<UserEntity> {
return await this.createQueryBuilder().where({ id: userId }).getOne();
}
}
{
"data": {
"getAllRoomInfo": [
{
"roomId": "23bfddea-ef11-413f-940d-0101fc8ab23c",
"users": [
{
"id": "ab5e6db1-2054-4fd2-9066-4ad1e407e903",
"name": "tom"
},
{
"id": "268311cb-66ce-486c-85c4-1bc6e0adbd6a",
"name": "ciara"
},
{
"id": "323d22e7-0edd-4d4f-ae3b-cb8b7ce42a97",
"name": "json"
},
{
"id": "ba176fc3-b860-4cae-9703-c9030212f5e7",
"name": "daniel"
}
]
},
{
"roomId": "68e75332-c884-4f20-b05a-2795bf03945a",
"users": [
{
"id": "9220a7f9-c958-48b7-80ff-15d1b462ae5a",
"name": "jonny"
},
{
"id": "7c7c2b9d-337c-4da7-b0b1-3f88b1adc486",
"name": "hardy"
}
]
},
{
"roomId": "1ec747ab-b761-4889-89bb-6c6516f4874d",
"users": []
}
]
}
}
여기서 주의 깊게 봐야할 부분은 repository
이다
현재 나는 typeorm 버전이 3.0.0 이상이다
대부분의 사람들이 알겠지만, typeOrm 버전이 올라가면서 @EntityRepository()
가 없어졌다.
구글링을 해보면 다들 custom repository 를 만들어서, @InjectRepository()
를 이용해서 각 모듈마다 주입을 해주는 방법을 구현하고 있는데, 생각보다 너무 복잡해서 굳이 이렇게까지 어렵게 구현하라고 nest 가 @EntityRepository()
를 없애진 않았을 것이다.
찾아보다가 좋은 방법을 찾았다.
https://stackoverflow.com/questions/72549668/how-to-do-custom-repository-using-typeorm-mongodb-in-nestjs
위에 나와있는 repository 코드가 다음 레퍼런스를 참고해서 만들었다.
생성자 부분을 잘 보면, 필요한 엔티티를 부모 클래스인 Repository 로 넘겨주면 provider 로 사용할 수 있게 된다.
여기서 transaction 은 어떻게 처리하는지는 다음에 알아보겠다.
사진: Unsplash의Barn Images