0
0
Lập trình
Flame Kris
Flame Krisbacodekiller

Cách Thêm Cache Cho Dịch Vụ NestJS Dễ Dàng

Đăng vào 7 tháng trước

• 7 phút đọc

Chủ đề:

KungFuTech

Giới thiệu

Caching là một kỹ thuật cơ bản để cải thiện hiệu suất ứng dụng. Tuy nhiên, việc thực hiện nó một cách sạch sẽ mà không làm lẫn lộn logic kinh doanh có thể là một thách thức. Bài viết này sẽ hướng dẫn bạn cách triển khai các giải pháp caching thanh lịch cho cả controller và service sử dụng decorators và lập trình hướng khía cạnh (AOP).

Vấn đề

Bạn có một phương thức dịch vụ với các truy vấn cơ sở dữ liệu nặng. Bạn muốn có caching có thể tái sử dụng, tách biệt với logic kinh doanh và dễ bảo trì.

Giải pháp 1: Cách tiếp cận Interceptor truyền thống

Giải pháp rõ ràng theo cách "NestJS" là sử dụng một decorator kết hợp với một interceptor tùy chỉnh.

typescript Copy
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, SetMetadata } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Observable, of } from "rxjs";
import { tap } from "rxjs/operators";
import { CacheService } from "./cache.service";

export type KeyFn = (request: Request) => string;
export type Key = string | KeyFn;
export type CacheOptions = { key: Key; ttl?: number };

const CACHE_OPTIONS = "CACHE_OPTIONS";
const Cached = (key: Key, ttl = 0) => SetMetadata(CACHE_OPTIONS, { key, ttl });

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private readonly reflector: Reflector, private readonly cacheService: CacheService) {}

  async intercept(context: ExecutionContext, next: CallHandler): Promise<Observable<any>> {
    const options = this.reflector.get<CacheOptions>(CACHE_OPTIONS, context.getHandler());
    if (!options) return next.handle();

    const { key, ttl } = options;
    const cacheKey = typeof key === "string" ? key : key(context.switchToHttp().getRequest());
    const cached = await this.cacheService.get(cacheKey);

    if (cached) return of(cached);

    return next.handle().pipe(tap(async (data) => {
      if (data) await this.cacheService.set(cacheKey, data, ttl);
    }));
  }
}

Cách sử dụng trong Controllers

typescript Copy
@Controller("/api/posts")
@UseInterceptors(CacheInterceptor)
export class PostsController {
  @Get(":id")
  @Cached((req) => `posts:${req.params.id}`)
  getPost(@Param("id") id: string) {
    // ...
  }
}

Kết hợp Interceptor và Decorator

Để tiện lợi, bạn có thể kết hợp interceptor và decorator thành một decorator duy nhất:

typescript Copy
import { applyDecorators, SetMetadata, UseInterceptors } from "@nestjs/common";

export function Cached(key: Key, ttl = 0) {
  return applyDecorators(SetMetadata(CACHE_OPTIONS, { key, ttl }), UseInterceptors(CacheInterceptor));
}

Giới hạn: Interceptors chỉ hoạt động cho các controller HTTP, không cho các phương thức dịch vụ.

Giải pháp 2: Cách tiếp cận dựa trên AOP

Tại đây, @toss/nestjs-aop sẽ cứu cánh. Nó cho phép bạn sử dụng decorators ở bất kỳ đâu trong ứng dụng của bạn. Cấu hình rất đơn giản:

Bước 1: Cài đặt gói

bash Copy
pnpm add @toss/nestjs-aop

Bước 2: Nhập AopModule

typescript Copy
@Module({
  imports: [AopModule],
})
export class AppModule {}

Bước 3: Tạo ký hiệu Decorator

typescript Copy
export const CACHE_DECORATOR = Symbol("CACHE_DECORATOR");

Bước 4: Triển khai LazyDecorator

typescript Copy
@Aspect(CACHE_DECORATOR)
export class CacheDecorator implements LazyDecorator<any, CacheOptions> {
  constructor(private readonly cacheService: CacheService) {}

  wrap({ method, metadata: { key, ttl } }: WrapParams<any, CacheOptions>) {
    return async (...args: any) => {
      const cacheKey = typeof key === "string" ? key : key(...args);
      const cached = await this.cacheService.get(cacheKey);
      if (cached) return cached;

      const data = await method(...args);
      await this.cacheService.set(cacheKey, data, ttl);
      return data;
    };
  }
}

Bước 5: Đăng ký Decorator

typescript Copy
@Module({
  providers: [CacheDecorator],
})
export class CacheModule {}

Bước 6: Tạo hàm Decorator

typescript Copy
export const Cached = (key: Key, ttl = 0) => createDecorator(CACHE_DECORATOR, { key, ttl });

Bước 7: Sử dụng bất kỳ đâu trong ứng dụng của bạn

typescript Copy
export class PostsService {
  @Cached((id: string) => `posts:${id}`)
  getPostById(id: string) {
    // ...
  }
}

Thiết lập hạ tầng Cache

Để mọi thứ hoạt động, bạn cần thiết lập một module cache toàn cục. Dưới đây là một ví dụ đầy đủ sử dụng Redis:

typescript Copy
import { Global, Module } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { CacheService } from "./cache.service";
import { CacheModule as NestCacheModule } from "@nestjs/cache-manager";
import { createKeyv } from "@keyv/redis";
import { EnvironmentVariables } from "../../environment";
import { CacheDecorator } from "./cache.decorator";

@Global()
@Module({
  imports: [NestCacheModule.registerAsync({
    useFactory: async (configuration: ConfigService<EnvironmentVariables, true>) => {
      const host = configuration.get("APP_REDIS_HOST", { infer: true });
      const port = configuration.get("APP_REDIS_PORT", { infer: true });
      return {
        stores: [createKeyv(`redis://${host}:${port}`)],
      };
    },
    inject: [ConfigService],
    isGlobal: true,
  })],
  providers: [CacheService, CacheDecorator],
  exports: [CacheService],
})
export class CacheModule {}

Và tương ứng với cache.service.ts:

typescript Copy
import { Injectable, Inject } from "@nestjs/common";
import { Cache } from "cache-manager";
import { CACHE_MANAGER } from "@nestjs/cache-manager";

@Injectable()
export class CacheService {
  constructor(@Inject(CACHE_MANAGER) private readonly cache: Cache) {}

  async get<T>(key: string): Promise<T | undefined> {
    return this.cache.get<T>(key);
  }

  async set<T>(key: string, value: T, ttl?: number): Promise<void> {
    await this.cache.set<T>(key, value, ttl);
  }

  async delete(key: string): Promise<void> {
    await this.cache.del(key);
  }
}

Mở rộng AOP: Vượt ra ngoài Caching

Sức hấp dẫn của AOP là bạn có thể áp dụng cùng một mẫu cho các mối quan tâm cắt ngang khác. Hãy khám phá hai ví dụ thực tế khác cho thấy sức mạnh của cách tiếp cận này.

Decorator Logging

typescript Copy
export const LOG_DECORATOR = Symbol("LOG_DECORATOR");
export type LogOptions = { level?: "log" | "debug" | "warn" | "error"; includeArgs?: boolean; includeResult?: boolean; };
@Aspect(LOG_DECORATOR)
export class LogDecorator implements LazyDecorator<any, LogOptions> {
  constructor(private readonly logger: Logger) {}

  wrap({ methodName, method, metadata }: WrapParams<any, LogOptions>) {
    const { level = "log", includeArgs = true, includeResult = false } = metadata;

    return async (...args: any[]) => {
      if (includeArgs) {
        this.logger[level](`Calling ${methodName} with args:`, args);
      } else {
        this.logger[level](`Calling ${methodName}`);
      }

      try {
        const result = await method(...args);

        if (includeResult) {
          this.logger[level](`${methodName} returned:`, result);
        } else {
          this.logger[level](`${methodName} completed successfully`);
        }

        return result;
      } catch (error) {
        this.logger.error(`${methodName} failed:`, error);
        throw error;
      }
    };
  }
}

export const Logged = (options: LogOptions = {}) => createDecorator(LOG_DECORATOR, options);

Decorator xác thực Zod

typescript Copy
import { z } from "zod";
import { BadRequestException, InternalServerErrorException } from "@nestjs/common";
export const VALIDATE_DECORATOR = Symbol("VALIDATE_DECORATOR");
export type ValidateOptions = { input?: z.ZodSchema; output?: z.ZodSchema; };
@Aspect(VALIDATE_DECORATOR)
export class ValidateDecorator implements LazyDecorator<any, ValidateOptions> {
  wrap({ method, metadata }: WrapParams<any, ValidateOptions>) {
    const { input, output } = metadata;
    return async (...args: any[]) => {
      if (input) {
        try {
          input.parse(args[0]);
        } catch (error) {
          throw new BadRequestException(`Validation failed: ${error.message}`);
        }
      }

      const result = await method(...args);

      if (output) {
        try {
          return output.parse(result);
        } catch (error) {
          throw new InternalServerErrorException(`Output validation failed: ${error.message}`);
        }
      }

      return result;
    };
  }
}

export const Validated = (options: ValidateOptions) => createDecorator(VALIDATE_DECORATOR, options);

Kết luận

Lập trình hướng khía cạnh với NestJS cung cấp một cách mạnh mẽ để triển khai các mối quan tâm cắt ngang như caching, logging và validation. Những lợi ích bao gồm:

  • Phân tách sạch sẽ: Logic kinh doanh vẫn tập trung và không bị lộn xộn
  • Tái sử dụng: Decorators hoạt động trên các controller, dịch vụ và bất kỳ lớp tiêm nào
  • Tính ghép lại: Nhiều decorators có thể kết hợp một cách liền mạch
  • Khả năng bảo trì: Thay đổi logic cắt ngang được tập trung

Trong khi các interceptors truyền thống hoạt động tốt cho các kịch bản cụ thể HTTP, các decorators AOP cung cấp sự linh hoạt để nâng cao bất kỳ phương thức nào trong ứng dụng của bạn. Cách tiếp cận này mở rộng một cách đẹp đẽ khi ứng dụng của bạn phát triển, giữ cho mã của bạn sạch sẽ và các mối quan tâm được tách biệt hợp lý.

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào