Al finalizar este tutorial el estudiante estará en capacidad de implementar una técnica de Caching en un proyecto de Nest.js.
Para realizar este taller Ud. debe:
Para poder implementar caching en Nest.js se deben instalar dos dependencias: cache-manager y @Types/cache-manager.
$ npm install --save cache-manager
$ npm install --save -D @types/cache-manager
Luego de que estos paquetes se hayan instalado debemos importar el módulo CacheModule
mediante su método register()
. Esto lo podemos hacer tanto individualmente en cada módulo del proyecto o lo podemos hacer globalmente.
Para hacerlo individualmente, debemos agregar el módulo anteriormente mencionado a la lista de imports
. Por ejemplo, este sería el código en el módulo MuseumModule
.
import { CacheModule, Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { MuseumEntity } from './museum.entity';
import { MuseumService } from './museum.service';
import { MuseumController } from './museum.controller';
@Module({
imports: [TypeOrmModule.forFeature([MuseumEntity]), CacheModule.register()],
providers: [MuseumService],
controllers: [MuseumController],
})
export class MuseumModule {}
/* archivo: src/museum/museum.module.ts */
El método register
también puede recibir otros parámetros opcionales. Uno de ellos es ttl
que permite indicar un tiempo de vencimiento de caché (en segundos)..
El tiempo de vencimiento de caché por defecto es de 5 segundos, es decir que al transcurrir 5 segundos de haber guardado el caché, este se eliminará.
Para el manejo del caché existen dos estrategias: desde el controlador del recurso o desde el servicio (lógica). Para este tutorial tomaremos la segunda opción.
El manejo del caché desde la lógica se realiza mediante la inyección de dependencias de un CacheManager.
constructor(
@InjectRepository(MuseumEntity)
private readonly museumRepository: Repository<MuseumEntity>,
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache
){}
/*archivo src/museum/museum.service.ts*/
Las importaciones de los elementos utilizados en esta inyección, son las siguientes:
import { CACHE_MANAGER, Inject } from '@nestjs/common';
import { Cache } from 'cache-manager';
Luego de haber inyectado la dependencia en el constructor del servicio, se deben modificar los métodos tendrán acceso al caché. La dependencia que hemos inyectado posee diferentes métodos como get
y set
para obtener y agregar elementos al caché.
Los elementos en el caché se identifican con una llave primaria, que se debe enviar a estos métodos.
Para ilustrar esto, veamos el código del servicio de la clase MuseumService
. Acá estamos implementando Caching mediante el CacheManager en el método findAll
:
...
@Injectable()
export class MuseumService {
cacheKey: string = "artists";
constructor(
@InjectRepository(MuseumEntity)
private readonly museumRepository: Repository<MuseumEntity>,
@Inject(CACHE_MANAGER)
private readonly cacheManager: Cache
){}
async findAll(): Promise<MuseumEntity[]> {
const cached: MuseumEntity[] = await this.cacheManager.get<MuseumEntity[]>(this.cacheKey);
if(!cached){
const museums: MuseumEntity[] = await this.museumRepository.find({ relations: ["artworks", "exhibitions"] });
await this.cacheManager.set(this.cacheKey, museums);
return museums;
}
return cached;
}
...
Lo primero que se hace es tomar los datos del caché que estén almacenados para la llave Museums. Si no existen datos en el caché se toman los datos del repositorio, se almacenan en caché y se retornan al cliente. Si los datos existen en caché se retornan directamente desde el caché.
Para demostrar el potencial de optimización que ofrece el Caching en la aplicación, podemos realizar una prueba de desempeño del API. Para esto, usaremos la herramienta Mockaroo que nos permite generar datos aleatorios para una tabla de la base de datos.
Para generar 1000 filas aleatorias para la tabla museum_entity podemos usar la siguiente configuración:
En la parte inferior seleccione el formato SQL, en el nombre de tabla ingrese museum_entity y haga clic en el botón "DOWNLOAD DATA". Esto descargará un archivo que contendrá un script para insertar datos en la tabla. Repita este proceso para generar 10 archivos. Luego, concatene las instrucciones en un solo archivo, y con ayuda de PgAdmin ejecute el script contra la base de datos para insertar la información.
Apache JMeter es una herramienta de pruebas utilizada en varios entornos, uno de los cuales está relacionado con la medición del rendimiento de una API REST.
Una vez descargada la herramienta, ejecutamos el archivo bin/jmeter.bat.
Lo primero que se debe hacer es definir un plan de pruebas o "Test Plan". Al iniciar la aplicación ya tenemos un plan de pruebas por defecto creado, entonces comenzaremos a configurarlo; para esto seleccionamos en el bloque izquierdo la opción "Test Plan" > clic derecho > Add Threads (Users) > Thread Group.
Esta opción creará un grupo de threads o "usuarios" que accederán simultáneamente al API mediante peticiones HTTP. En la opción Number of Threads (users) incluya el número 20. Esto ejecutará 20 peticiones sucesivas.
Siguiendo este orden, ahora se debe definir las peticiones HTTP que queremos probar; para esto seleccionamos Test Plan > Thread Group > Add > Sampler > HTTP Request.
En este caso, si queremos probar el endpoint "/api/v1/museums" definiremos los siguientes datos en la petición:
Por último debemos crear también un árbol de visualización de resultados, en donde podamos observar los resultados de las peticiones realizadas. Para esto, hacemos clic derecho sobre HTTP Request y seleccionamos Add > Listener > View Results Tree.
Una vez configurado el plan de pruebas, debemos guardarlo haciendo clic en la esquina superior izquierda, seleccionando el símbolo de guardar.
Una vez guardado, podemos ejecutar el plan de pruebas, seleccionando en la barra superior el ícono de correr (triángulo verde) y observaremos los resultados de las peticiones, el cuerpo de respuesta y el tiempo que tardó cada una de las peticiones.
Cuando realizamos la medición en este tutorial tuvimos un tiempo de 219 ms para la primera petición y uno de 29 ms para la última. De este modo se comprueba el efecto que tiene sobre el rendimiento del API la incorporación de caching.
Otra opción para mejorar el desempeño del API es utilizar una base de datos en memoria que guarde los datos ya sea durante el tiempo de ejecución de la aplicación, o incluso persistiendo el caché, de modo que cuando se reinicie la aplicación, el caché aún exista. La implementación más común para este tipo de prácticas en NestJS es usar Redis, una base de datos en memoria; sin embargo, esta base de datos solo tiene soporte para los sistemas operativos MacOS y Linux, pero no para Windows
Otra opción similar es utilizar SQLite para generar una base de datos en memoria, la cual sí cuenta con soporte para Windows.
Para esto, se debe instalar un nuevo paquete mediante el comando:
$ npm install --save cache-manager-sqlite
Este nuevo paquete contiene un manejador de caché específico que hace uso de SQLite.
Para utilizar SQLite como almacenador de caché se deben incluir los siguientes atributos a la importación del módulo CacheModule
en MuseumModule
:
import * as sqliteStore from 'cache-manager-sqlite';
imports: [
CacheModule.register({
store: sqliteStore,
path: ':memory:',
options: {
ttl: 5
},
})
]
/* archivo: src/museum/museum.module.ts */
store
representa cuál será la fuente de almacenamiento que se utilizará para almacenar el caché; en este caso SQLite.path
recibe como parámetro la ubicación de la base de datos (archivo) o el texto ':memory:'
para indicar que se desea que se almacene en memoria en vez de en un archivo. Loptions.ttl
definimos un tiempo de vencimiento del caché de 5 segundos.