我尝试实现了用于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秒,但请根据响应内容的变更频率和更新延迟的可接受范围进行调整。

bannerAds