Як використовувати кеш у сервісах NestJS
«Кеш це як спеції: трошки додав — і все смачно, але переборщив — і вже не зрозумієш, що це за страва!»
Вступ
Якщо ви працюєте з NestJS, то напевно знаєте, що кешування — це маст-хев для продуктивного додатку. Без кешу ваш сервер може працювати, як старий принтер: думає довше, ніж друкує.
Сьогодні ми розберемо, як правильно додати кеш у NestJS, створимо глобальний модуль кешування, а також напишемо декоратор Cached()
для кешування викликів функцій.
Кешування на рівні сервісів vs контролерів
NestJS надає вбудовані засоби для кешування відповідей на рівні контролерів за допомогою
CacheInterceptor
. Цей підхід дозволяє кешувати відповіді ендпоїнтів, зменшуючи навантаження на сервер та прискорюючи обробку запитів.Однак, кешування можна реалізувати не лише на рівні контролерів, але й безпосередньо в сервісах. Цей підхід дозволяє кешувати результати виконання методів сервісів, що особливо корисно при частому виклику одних і тих самих методів з однаковими параметрами. Використання декоратора
Cached()
для кешування методів сервісів забезпечує більш гнучке та ефективне управління кешем, оскільки дозволяє контролювати кешування на рівні бізнес-логіки.
Крок 1: Встановлення необхідних пакетів
Для початку потрібно встановити cache-manager
та @nestjs/cache-manager
:
npm install cache-manager @nestjs/cache-manager
Якщо ви хочете використовувати Redis як бекенд для кешу (а це гарна ідея), то додайте ще @keyv/redis
:
npm install @keyv/redis
Крок 2: Створюємо AppCacheModule
та CacheService
CacheService
Щоб керувати кешем, створимо сервіс кешування:
// cache.service.ts import { CACHE_MANAGER } from '@nestjs/cache-manager' import { Inject, Injectable, OnModuleInit } from '@nestjs/common' import { Cache } from 'cache-manager' @Injectable() export class CacheService implements OnModuleInit { private static instance: CacheService constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {} onModuleInit() { CacheService.instance = this } static getInstance(): CacheService { if (!CacheService.instance) { throw new Error('CacheService is not initialized yet!') } return CacheService.instance } async get<T>(key: string): Promise<T | null> { return this.cacheManager.get<T>(key) } async set<T>(key: string, value: T, ttlInMs: number): Promise<void> { await this.cacheManager.set(key, value, ttlInMs) } }
Тепер у нас є синглтон-сервіс для кешування, який ми можемо використовувати в усьому додатку.
AppCacheModule
Щоб зробити кеш глобальним, створимо модуль AppCacheModule
:
// cache.module.ts import { Module, Global } from '@nestjs/common' import { CacheModule } from '@nestjs/cache-manager' import { CacheService } from './cache.service' import { ConfigModule, ConfigService } from '@nestjs/config' import { EnvVariables } from '@common/validators/env.validator' import KeyvRedis from '@keyv/redis' @Global() @Module({ imports: [ CacheModule.registerAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: async (configService: ConfigService<EnvVariables, true>) => ({ stores: [ new KeyvRedis({ url: `redis://${configService.get('REDIS_HOST')}:${configService.get('REDIS_PORT')}`, }), ], }), isGlobal: true, }), ], providers: [CacheService], exports: [CacheService], }) export class AppCacheModule {}
Тепер цей модуль можна підключати в AppModule
, і модуль кешування буде доступний у всьому додатку.
Крок 3: Створюємо декоратор Cached()
Щоб не писати логіку кешування вручну в кожному методі, створимо декоратор Cached()
:
// cached.decorator.ts import { ClassConstructor, plainToInstance } from 'class-transformer' import { CacheService } from '../cache.service' export function Cached<T>( classType: ClassConstructor<T>, ttlInSeconds: number, key?: string | ((...args: unknown[]) => string), ) { return function ( target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<(...args: unknown[]) => Promise<T>>, ) { const originalMethod = descriptor.value if (!originalMethod) { throw new Error(`Decorator @Cached can only be applied to methods.`) } descriptor.value = async function (...args: unknown[]): Promise<T> { let cacheKey = `cache:${propertyKey}:` if (typeof key === 'function') { cacheKey += key(...args) } else if (typeof key === 'string') { cacheKey += key } else { cacheKey += JSON.stringify(args) } const cacheManager = CacheService.getInstance() const cached = await cacheManager.get<T>(cacheKey) if (cached !== null) { // Deserialize plain object into an instance of classType return plainToInstance(classType, cached, { enableImplicitConversion: true, }) } const result = await originalMethod.apply(this, args) await cacheManager.set<T>(cacheKey, result, ttlInSeconds * 1000) return result } return descriptor } }
Цей декоратор автоматично кешує результати функції. Якщо значення є в кеші — повертаємо його, інакше виконуємо метод і зберігаємо результат. Також результат, який зберігається в кеші, десеріалізується у відповідний клас, щоб зберегти правильний тип.
Приклад використання декоратора Cached()
Краще всього використовувати кешування на методах, які виконують важкі обчислення або витяжку великої інформації з основної бази даних. Нижче наведений лише приклад використання:
import { Injectable } from '@nestjs/common'; import { Cached } from './cached.decorator'; @Injectable() export class UserService { // Приклад 1: кешуємо дані юзера на 2 хвилини @Cached(User, 120) async getUser(id: number): Promise<User> {} // Приклад 2: кешуємо дані юзера на 10 хвилин, використовуючи динамічний ключ @Cached(User, 600, (id: number) => `user:${id}`) async getUser(id: number): Promise<User> {} // Приклад 3: кешуємо список всіх юзерів на 5 хвилин з фіксованим ключем @Cached(User, 300, 'all_users'): Promise<User[]> async getUsers() {} }
Загальна структура модуля Cache
├── cache/ │ ├── decorators/ │ │ └── cached.decorator.ts │ ├── cache.module.ts │ └── cache.service.ts
Висновок
— Ми створити глобальний модуль та сервіс роботи з кешуванням в NestJS.
— Реалізували декоратор Cached()
для зручного кешування результату функцій.
— Тепер ваші запити оброблятимуться миттєво, а сервер більше не буде нагадувати пароварку під час навантаження.
10 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів