Chapter 26

techniques-caching

kisihara.c
kisihara.c
2021.04.04に更新

キャッシュ

キャッシュは貴方のアプリのパフォーマンスを向上させるグレートでシンプルな技術だ。それは一時的なデータストアとして機能し、ハイパフォーマンスなデータアクセスを実現する。

インストール

まず必要なパッケージをインストールしよう。

$ npm install cache-manager
$ npm install -D @types/cache-manager

インメモリキャッシュ

Nestは様々なキャッシュストレージプロバイダのための統一されたAPIを提供している。ビルトインされているのは、インメモリデータストアだ。しかし、Redisのようなより包括的なソリューションに簡単に切り替えることができる。

キャッシュを有効にするには、CacheModuleをインポートして、そのregister()メソッドを呼び出す。

import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';

@Module({
  imports: [CacheModule.register()],
  controllers: [AppController],
})
export class AppModule {}

キャッシュストアと対話

キャッシュマネージャのインスタンスを操作するには、以下のようにCACHE_MANAGERトークンを使用してクラスにインジェクションする。

constructor(@Inject(CACHE_MANAGER) private cacheManager: Cache) {}

HINT
Cacheクラスは、@nestjs/commonパッケージのCACHE_MANAGERトークンを使用して、cache-managerからインポートされる。

chache-managerパッケージの)Cacheインスタンスのgetメソッドは、キャッシュから項目を取得するために使用される。項目がキャッシュに存在しない場合は、例外がスローされる。

const value = this.cacheManager.get('key');

アイテムをキャッシュに追加するには、setメソッドを使用する。

await this.cacheManager.set('key', 'value');

キャッシュのデフォルトの有効期限は5秒だ。

以下のように、この特定のキーのTTL(有効期限)を手動で指定することができる。

await this.cacheManager.set('key', 'value', { ttl: 1000 });

キャッシュの有効期限を無効にするには、設定プロパティttlnullにする。

await this.cacheManager.set('key', 'value', { ttl: null });

キャッシュからアイテムを削除するには、delメソッドを使う。

await this.cacheManager.del('key');

キャッシュ全体をクリアするには、resetメソッドを使う。

await this.cacheManager.reset();

オートキャッシングレスポンス

WARNING
GraphQLアプリケーションでは、インターセプタはフィールドリゾルバごとに個別に実行される。そのため、インターフェプタを使用してレスポンスをキャッシュするCacheModuleは正しく動作しない。

オートキャッシングレスポンスを有効にするには、データをキャッシュしたい場所にCacheInterceptorを結びつけるだけだ。

@Controller()
@UseInterceptors(CacheInterceptor)
export class AppController {
  @Get()
  findAll(): string[] {
    return [];
  }
}

WARNING
GETエンドポイントのみがキャッシュされる。また、ネイティブレスポンスオブジェクト(@Res())をインジェクションするHTTPサーバルートではキャッシュインターセプターが使えない。詳細はこちら

必要なボイラープレートの量を減らす為、CacheInterceptorをすべてのエンドポイントにグローバルにバインドできる。

import { CacheModule, Module, CacheInterceptor } from '@nestjs/common';
import { AppController } from './app.controller';
import { APP_INTERCEPTOR } from '@nestjs/core';

@Module({
  imports: [CacheModule.register()],
  controllers: [AppController],
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: CacheInterceptor,
    },
  ],
})
export class AppModule {}

キャッシュのカスタマイズ

すべてキャッシュされたデータは、独自の有効期限(TTL)を持つ。デフォルトの設定をカスタマイズするには、register()メソッドにオプションオブジェクトを通す。

CacheModule.register({
  ttl: 5, // 秒
  max: 10, // キャッシュされるアイテムの最大数
});

グローバルキャッシュのオーバーライド

グローバルキャッシュが有効な場合、キャッシュエントリは、ルートパスに基づいて自動生成されたCacheKeyの下に保存される。特定のキャッシュ設定(@ChacheKey()および@ChacheTTL())をメソッドごとにオーバーライドする事で、個々のコントローラメソッドのキャッシュ方針をカスタマイズすることができる。これは異なるキャッシュストアを使用している場合に有効となる。

@Controller()
export class AppController {
  @CacheKey('custom_key')
  @CacheTTL(20)
  findAll(): string[] {
    return [];
  }
}

HINT
@CacheKey@CacheTTLデコレータは@nestjs/commonパッケージからインポートされている。

@CacheKey()デコレータは対応する@CacheTTL()デコレータと一緒に使ってもいいし、使わなくても良い。それぞれを単独で使う事もできる。デコレータでオーバーライドされていない場合は、グローバルに登録されているデフォルト値が使用される(前段「キャッシュのカスタマイズ」を参照の事)。

WebSocketsとマイクロサービス

キャッシュインターセプターは、使用されているトランスポート方法に関わらず、WebSocketサブスクライバやマイクロサービスのパターンにも適用できる。

@CacheKey('events')
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
  return [];
}

しかしながら、キャッシュされたデータを保存・取得するためのキーを指定するには、追加で@CacheKey()デコレータが必要だ。また、全てを全てキャッシュすべきでない事も注意。単なるデータのクエリではなく、何らかのビジネスオペレーションを行うアクションは、キャッシュされてはならないだろう。

さらに、@CacheTTL()デコレータでキャッシュの有効期限(TTL)を指定する事もできる。これはグローバルなデフォルトのTTLの値を上書きする。

@CacheTTL(10)
@UseInterceptors(CacheInterceptor)
@SubscribeMessage('events')
handleEvent(client: Client, data: string[]): Observable<string[]> {
  return [];
}

HINT
CacheTTL()デコレータは、対応する@CacheKey()デコレータと一緒に、もしくは単独で使用することができる。

トラッキングの調整

デフォルトでは、Nestは(HTTPアプリでは)リクエストのURL、または(websocketsとマイクロサービスのアプリで@CacheKey()デコレータで設定した場合)キャッシュキーを使用して、キャッシュレコードとエンドポイントを関連付ける。とはいえ、HTTPヘッダー(プロファイルエンドポイントを適切に識別する為のAuthorizationなど)を使用するなど、異なる要素に基づいてトラッキングを設定したい場合もあるだろう。

これを実現するには、CacheInterceptorのサブクラスを作成し、trackBy()メソッドをオーバーライドする。

@Injectable()
class HttpCacheInterceptor extends CacheInterceptor {
  trackBy(context: ExecutionContext): string | undefined {
    return 'key';
  }
}

別のストア

このサービスは内部でcache-managerを使用している。cache-managerパッケージは、例えばRedisのような便利なストアを幅広くサポートしている。サポートされているストアの完全なリストはこちら。Redisをセットアップするには、パッケージと対応するオプションをregister()メソッドに渡すだけだ。

import * as redisStore from 'cache-manager-redis-store';
import { CacheModule, Module } from '@nestjs/common';
import { AppController } from './app.controller';

@Module({
  imports: [
    CacheModule.register({
      store: redisStore,
      host: 'localhost',
      port: 6379,
    }),
  ],
  controllers: [AppController],
})
export class AppModule {}

非同期設定

モジュールのオプションをコンパイル時に静的に渡すのではなく、非同期で渡したい場合がある。この場合registerAsync()メソッドを使う。このメソッドには、非同期瀬底を扱う方法がいくつか用意されている。

1つの方法は、ファクトリ関数を使用する事だ。

CacheModule.registerAsync({
  useFactory: () => ({
    ttl: 5,
  }),
});

このファクトリは、他の非同期モジュールファクトリと同様に動作する。async化できて、injectで依存性をインジェクションできる。

CacheModule.registerAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => ({
    ttl: configService.get('CACHE_TTL'),
  }),
  inject: [ConfigService],
});

代わりに、useClassメソッドを使用する事もできる。

CacheModule.registerAsync({
  useClass: CacheConfigService,
});

上記のコードでは、CacheModule内にCacheConfigServiceをインスタンス化し、これを使用してオプションオブジェクトを取得する。CacheConfigServiceは、構成オプションを提供するために、CacheOptionsFactoryインターフェイスを実装する必要がある。

@Injectable()
class CacheConfigService implements CacheOptionsFactory {
  createCacheOptions(): CacheModuleOptions {
    return {
      ttl: 5,
    };
  }
}

別のモジュールからインポートされた既存の構成プロバイダを使用したい場合は、useExisting構文を使用する。

CacheModule.registerAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
});

CacheModuleは、自分自身のインスタンスを作成するのではなく、インポートされたモジュールを検索し、作成済みのConfigServiceを再利用する。

サンプル

動作するサンプルはこちら