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
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
@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
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
pnpm add @toss/nestjs-aop
Bước 2: Nhập AopModule
typescript
@Module({
imports: [AopModule],
})
export class AppModule {}
Bước 3: Tạo ký hiệu Decorator
typescript
export const CACHE_DECORATOR = Symbol("CACHE_DECORATOR");
Bước 4: Triển khai LazyDecorator
typescript
@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
@Module({
providers: [CacheDecorator],
})
export class CacheModule {}
Bước 6: Tạo hàm Decorator
typescript
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
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
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
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
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
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ý.