@nestjs/graphql でdataloaderを使う

GraphQLのN+1問題を解決するための策として[Dataloader](https://github.com/graphql/dataloader)があります。今回はNestJSでDataloaderをProviderとして登録し、使用する方法を紹介します。

**Note: Dataloaderの知識がある程度ある人向けに書いています。**

## 本記事のゴール

このResolverクラスを

```ts
@Resolver("Query")
export class UserResolver {

  constructor(private userService: UserService) {}

  @ResolveProperty()
  public async user(@Args("id") id: number): Promise<User> {
    return this.userService.findById(id);
  }
}
```

次のように、DataloaderクラスをServiceの代わりにInjectできるようにします。

```ts
@Resolver("Query")
export class UserResolver {

  constructor(private userDataloader: UserDataloader) {}

  @ResolveProperty()
  public async user(@Args("id") id: number): Promise<User> {
    return this.userDataloader.load(id);
  }
}
```

## 本記事で覚えること

覚えることは1つです。「Providerに`Scope.REQUEST`を使ってリクエストごとにインスタンスを再生成する」ということを頭に入れておけばいいです。

## Dataloaderクラスの作成

汎用性を高めるために`BaseDataloader`クラスを作成します。

```ts
import * as DataLoader from "dataloader";

export abstract class BaseDataloader<K, V> extends Object {
  protected dataloader: DataLoader<K, V> = new DataLoader<K, V>(this.batchLoad.bind(this));

  public clear(key: K): DataLoader<K, V> {
    return this.dataloader.clear(key);
  }

  public clearAll(): DataLoader<K, V> {
    return this.dataloader.clearAll();
  }

  public async load(key: K): Promise<V> {
    return this.dataloader.load(key);
  }

  public async loadMany(keys: K[]): Promise<V[]> {
    return this.dataloader.loadMany(keys);
  }

  public prime(key: K, value: V): DataLoader<K, V> {
    return this.dataloader.prime(key, value);
  }

  protected abstract async batchLoad(keys: K[]): Promise<(V | Error)[]>;
}
```

`Dataloader`クラスを作成します。`@Injectable({ scope: Scope.REQUEST })` というようにscopeを必ず`Scope.REQUEST`にします。これでリクエストごとにDataloaderが初期化されます。

```ts
@Injectable({ scope: Scope.REQUEST })
export class UserDataloader extends BaseDataloader<number, User> {

  constructor(private readonly userService: UserService) {
    super();
  }

  protected async batchLoad(keys: number[]): Promise<(User | Error)[]> {
    return this.userService.findByIds(keys);
  }
}
```

あとはModuleにproviderとして登録するだけです。

```ts
@Module({
  imports: [TypeOrmModule.forFeature([UserRepository])],
  providers: [UserService, UserDataloader, UserResolver]
})
export class UserModule {}
```

これで、次のようにDataloaderを使用することが出来るようになります。

```ts
@Resolver("Query")
export class UserResolver {

  constructor(private userDataloader: UserDataloader) {}

  @ResolveProperty()
  public async user(@Args("id") id: number): Promise<User> {
    return this.userDataloader.load(id);
  }
}
```

## Unitテスト時の注意

通常、TestingModuleからProviderを取得するときは `module.get` を使用すると思いますが、`Scope.REQUEST`を使用している場合は取得することができません。この場合、 `userDataloader`はundefinedになります。代わりに `module.resolve` を使用すれば解決します。

```ts
const module = await Test.createTestingModule({
  providers: [UserResolver, UserDataloader, UserService],
}).compile();
// UserResolverのインスタンスは取得できるが userDataloaderがundefeind
// module.resolve<UserResolver>(UserResolver)
const resolver = await module.resolve<UserResolver>(UserResolver);
```

また、本記事で紹介したBaseDataloaderクラスを継承したものにspyOnを使用するときは

```ts
const dataloader = await module.resolve<UserDataloader>(UserDataloader);
jest.spyOn(dataloader.constructor.prototype, "load").mockResolvedValueOnce(mock);
```