我尝试实现了用于NestJS GraphQL解析器的CacheInterceptor
在NestJS中,通过在控制器中使用UseInterceptors装饰器并指定CacheInterceptor,可以缓存控制器返回的响应数据,从而减轻服务器的处理负担。需要先引入CacheModule。
@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
@Get()
findAll(): string[] {
return [];
}
}
然而,正如官方文档的警告所述,该CacheInterceptor在GraphQL中不可用。
因此,我尝试自己制作了一个用于GraphQL的缓存拦截器。
自己编写了用于GraphQL的CacheInterceptor
请参考代码中的注释来了解处理的说明。
import { createHash } from "crypto";
import {
CACHE_MANAGER,
CallHandler,
ExecutionContext,
Inject,
Injectable,
Logger,
NestInterceptor,
} from "@nestjs/common";
import { GqlExecutionContext } from "@nestjs/graphql";
import { Cache } from "cache-manager";
import { GraphQLResolveInfo } from "graphql";
import { Observable, of, tap } from "rxjs";
@Injectable()
export class ResolverCacheInterceptor implements NestInterceptor {
private readonly logger = new Logger(ResolverCacheInterceptor.name);
constructor(
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache,
) {}
async intercept(ec: ExecutionContext, next: CallHandler): Promise<Observable<unknown>> {
const gec = GqlExecutionContext.create(ec);
// クエリの情報を取得
const { fieldName, parentType } = gec.getInfo<GraphQLResolveInfo>();
if (parentType.name !== "Query") {
// Query のフィールドではない場合はキャッシュしない
return next.handle();
}
// クエリの引数を取得
const args = gec.getArgs();
// キャッシュキーを生成
const key = this.generateCacheKey(fieldName, args);
// キャッシュ取得を試行
const cache = await this.getCache(key);
if (cache) {
// キャッシュがある場合、リゾルバメソッドを実行せず、キャッシュを返す
return of(cache);
}
return next.handle().pipe(
tap((res) => {
// リゾルバメソッドの戻り値(res)をキャッシュに保存
this.saveCache(key, res);
}),
);
}
/**
* クエリ名と引数からキャッシュキーを生成する
*/
private generateCacheKey(fieldName: string, args: Record<string, unknown>): string {
// キャッシュキーの長さをある程度の範囲に揃えるために
// 引数はJSON文字列にしてハッシュ値化したものをキャッシュキーに含める
const hash = createHash("md5").update(JSON.stringify(args)).digest("base64");
return `ResolverCache_${fieldName}_${hash}`;
}
/**
* キャッシュを取得する
* @param key キャッシュキー
*/
private async getCache(key: string): Promise<unknown> {
try {
const cache = await this.cacheManager.get(key);
if (cache) {
this.logger.debug(`cache hit ${key}`);
} else {
this.logger.debug(`cache miss ${key}`);
}
return cache;
} catch (err) {
this.logger.warn(`cache get failed ${key} ${err}`);
return null;
}
}
/**
* リゾルバメソッドの戻り値をキャッシュに保存する
* @param key キャッシュキー
* @param data リゾルバメソッドの戻り値
*/
private async saveCache(key: string, data: unknown ): Promise<void> {
try {
await this.cacheManager.set(key, data, { ttl: 60 });
this.logger.debug(`cache saved ${key}`);
} catch (err) {
this.logger.warn(`cache save failed ${key} ${err}`);
}
}
}
对于解析器方法的应用。
后面的事情就是通过将这个方法设置为解析器的 UseInterceptors 装饰器,来缓存第一次执行解析器方法的返回值,以后就不再执行解析器方法,而是返回缓存的内容。(虽然在下面的例子中返回了固定的字符串,不需要缓存…)
根据我们的协议,每个月的租金将会在最后一天前支付给房东。
type Query {
greeting(name: String!): String!
}
import { UseInterceptors } from "@nestjs/common";
import { Query, Resolver } from "@nestjs/graphql";
import { GraphQLError } from "graphql";
// 上記のResolverCacheInterceptorをimport
import { ResolverCacheInterceptor } from "./path/to/resolver-cache.interceptor";
@Resolver("Query")
export class QueryResolver {
@Query(() => String)
@UseInterceptors(ResolverCacheInterceptor) // これを追加
async greeting(@Args("name") name: string) {
return `Hello,${name}!`;
}
}
定制
在本文的实现示例中,我们假设GraphQL API始终返回相同的响应,前提是在相同的时间以相同的参数调用相同的查询。为了确保缓存键是唯一的,我们根据查询名称(Query的字段名)和参数设计了缓存键。
如果其他因素(例如保存在上下文中的认证信息)会导致响应内容发生变化,则需要修改处理方式,以便将这些信息包含在缓存键中,使其保持唯一。
另外,缓存时间设定为统一的60秒,但请根据响应内容的变更频率和更新延迟的可接受范围进行调整。