おそらくGraphQLにおいてキャッシュの仕組みを導入するならDataLoaderが最適解だとは思うのですが、nest.jsでDalaloaderを使用する方法が確立されていません。
※ 2020/01/20 追記
@nestjs/graphql でdataloaderを使う方法を書きました
参考issue:
https://github.com/nestjs/graphql/issues/20
https://github.com/nestjs/graphql/issues/2
DataLoaderの他に、nest.jsでは cache-managerを使用したCacheModuleも提供されています。ですがGraphQLで使用するサンプルはないし、私の環境でもエラーで動かなかったのでサポートしてなさそうです。
https://docs.nestjs.com/techniques/caching
という感じで、nest.jsにおけるキャッシュの仕組みはまだ確立されてなく、今後大きく変わりそうな気がしているのでDataLoaderやcache-managerを使用せず自前でキャッシュの仕組みを実装します。
CacheMap
Custom Decoratorの機能を使って、contextにmapを保持するようにします。contextにデータを保持するので、データは1リクエストの間だけ保持されます。DataLoaderのようなものですね。
https://docs.nestjs.com/graphql/tooling#custom-decorators
export const CacheMap = createParamDecorator(
(key: string = 'default', [root, args, ctx, info]) => {
if (!ctx.cacheMap) {
ctx.cacheMap = {} as { [key: string]: Map<string, any> };
}
if (!ctx.cacheMap[key]) {
ctx.cacheMap[key] = new Map<string, any>();
}
return ctx.cacheMap[key];
},
);
使い方
使い方はシンプルで簡単です。これでシンプルなキャッシュの仕組みができました。
@Resolver('Query')
export class QueryResolver {
constructor(private readonly service: Service) {}
@ResolveProperty()
public async users(@CacheMap() cache: Map<string, User[]>): Promise<User[]> {
const cacheKey = `users`;
const users = cache.has(cacheKey)
? cache.get(cacheKey)!
: await this.service.find();
cache.set(cacheKey, users);
return users;
}
}
余談1
CacheModuleのようにInterceptorを使ったキャッシュもできますが、GraphQLだとrelay、filter、orderByなどのクエリで加工することが多いので戻り値をキャッシュするのは無駄になると判断しました。
@Resolver('Query')
export class QueryResolver {
constructor(private readonly service: Service) {}
@ResolveProperty()
public async users(
@ConnectionArgs() connectionArgs: ConnectionArguments,
@CacheMap() cache: Map<string, User[]>): Promise<User[]> {
const cacheKey = `users`;
const users = cache.has(cacheKey)
? cache.get(cacheKey)!
: await this.service.find();
cache.set(cacheKey, users);
// 実際はここで更に加工
// https://www.npmjs.com/package/graphql-relay の connectionFromArray を使ったりしてる
return connectionFromArray(users, connectionArgs);
}
}
余談2
エイリアスを使った同じクエリに対してキャッシュは有効でないのでやっぱりDataLoaderほしい...
query getUsers {
admins: users(role: admin) {
id
firstName
lastName
phone
username
}
accountants: users(role: accountant) {
id
firstName
lastName
phone
username
}
}