Passer au contenu

Portées d’injection

Pour les personnes venant de différents langages de programmation, il peut être inattendu d’apprendre que dans Nest, presque tout est partagé à travers les requêtes entrantes. Nous avons un pool de connexions à la base de données, des services singleton avec un état global, etc. Rappelons que Node.js ne suit pas le modèle Multi-Threaded Stateless Model de requête/réponse dans lequel chaque requête est traitée par un thread séparé. Par conséquent, l’utilisation d’instances singleton est entièrement sûre pour nos applications.

Cependant, il existe des cas limites où une durée basée sur la requête peut être le comportement souhaité, par exemple, le cache par requête dans les applications GraphQL, le suivi des requêtes et la multi-location. Les portées d’injection fournissent un mécanisme pour obtenir le comportement souhaité du cycle de vie des fournisseurs.

Portée du fournisseur

Un fournisseur peut avoir l’une des portées suivantes :

PortéeDescription
DEFAULTUne seule instance du fournisseur est partagée à travers l’application entière. La durée de vie de l’instance est directement liée au cycle de vie de l’application. Une fois l’application initialisée, tous les fournisseurs singleton ont été instanciés. La portée singleton est utilisée par défaut.
REQUESTUne nouvelle instance du fournisseur est créée exclusivement pour chaque requête entrante. L’instance est collectée par le ramasse-miettes après que la requête ait été traitée.
TRANSIENTLes fournisseurs transitoires ne sont pas partagés entre les consommateurs. Chaque consommateur qui injecte un fournisseur transitoire recevra une nouvelle instance dédiée.

Utilisation

Spécifiez la portée d’injection en passant la propriété scope à l’objet d’options du décorateur @Injectable() :

Exemple d'utilisation de la portée de requête
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {}

De même, pour les fournisseurs personnalisés, définissez la propriété scope dans la forme longue pour l’enregistrement d’un fournisseur :

Enregistrement d'un fournisseur personnalisé avec portée transitoire
{
provide: 'CACHE_MANAGER',
useClass: CacheManager,
scope: Scope.TRANSIENT,
}

La portée singleton est utilisée par défaut et n’a pas besoin d’être déclarée. Si vous souhaitez déclarer un fournisseur en portée singleton, utilisez la valeur Scope.DEFAULT pour la propriété scope.

Portée du contrôleur

Les contrôleurs peuvent également avoir une portée, qui s’applique à tous les gestionnaires de méthodes de requête déclarés dans ce contrôleur. Comme pour la portée du fournisseur, la portée d’un contrôleur déclare sa durée de vie. Pour un contrôleur à portée de requête, une nouvelle instance est créée pour chaque requête entrante, et est collectée par le ramasse-miettes une fois la requête traitée.

Déclarez la portée du contrôleur avec la propriété scope de l’objet ControllerOptions :

Déclaration de la portée du contrôleur
@Controller({ path: 'cats', scope: Scope.REQUEST })
export class CatsController {}

Hiérarchie des portées

La portée REQUEST remonte la chaîne d’injection. Un contrôleur qui dépend d’un fournisseur à portée de requête sera, lui-même, à portée de requête.

Imaginez le graphique de dépendance suivant : CatsController <- CatsService <- CatsRepository. Si CatsService est à portée de requête (et les autres sont des singletons par défaut), le CatsController deviendra à portée de requête car il dépend du service injecté. Le CatsRepository, qui n’est pas dépendant, resterait en portée singleton.

Les dépendances à portée transitoire ne suivent pas ce schéma. Si un service singleton DogsService injecte un fournisseur transitoire LoggerService, il recevra une nouvelle instance de celui-ci. Cependant, DogsService restera à portée singleton, donc son injection n’entraînerait pas la résolution d’une nouvelle instance de DogsService. Dans le cas où c’est le comportement souhaité, DogsService doit être explicitement marqué comme TRANSIENT également.

Fournisseur de requête

Dans une application basée sur un serveur HTTP (par exemple, en utilisant @nestjs/platform-express ou @nestjs/platform-fastify), vous pouvez souhaiter accéder à une référence à l’objet de requête original lorsque vous utilisez des fournisseurs à portée de requête. Vous pouvez le faire en injectant l’objet REQUEST.

Le fournisseur REQUEST est à portée de requête, donc vous n’avez pas besoin d’utiliser explicitement la portée REQUEST dans ce cas.

Injection de l'objet de requête
import { Injectable, Scope, Inject } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { Request } from 'express';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(REQUEST) private request: Request) {}
}

En raison des différences de plateforme/protocole sous-jacentes, vous accédez à la requête entrante légèrement différemment pour les applications Microservice ou GraphQL. Dans GraphQL, vous injectez CONTEXT au lieu de REQUEST :

Injection du contexte dans une application GraphQL
import { Injectable, Scope, Inject } from '@nestjs/common';
import { CONTEXT } from '@nestjs/graphql';
@Injectable({ scope: Scope.REQUEST })
export class CatsService {
constructor(@Inject(CONTEXT) private context) {}
}

Ensuite, vous configurez votre valeur context (dans le GraphQLModule) pour contenir request comme sa propriété.

Fournisseur d’enquête

Si vous souhaitez obtenir la classe où un fournisseur a été construit, par exemple dans les fournisseurs de journalisation ou de métriques, vous pouvez injecter le jeton INQUIRER.

Injection du jeton d'enquête
import { Inject, Injectable, Scope } from '@nestjs/common';
import { INQUIRER } from '@nestjs/core';
@Injectable({ scope: Scope.TRANSIENT })
export class HelloService {
constructor(@Inject(INQUIRER) private parentClass: object) {}
sayHello(message: string) {
console.log(`${this.parentClass?.constructor?.name}: ${message}`);
}
}

Et utilisez-le comme suit :

Utilisation du fournisseur d'enquête
import { Injectable } from '@nestjs/common';
import { HelloService } from './hello.service';
@Injectable()
export class AppService {
constructor(private helloService: HelloService) {}
getRoot(): string {
this.helloService.sayHello('My name is getRoot');
return 'Hello world!';
}
}

Dans l’exemple ci-dessus, lorsque AppService#getRoot est appelé, "AppService: My name is getRoot" sera enregistré dans la console.

Performance

L’utilisation de fournisseurs à portée de requête aura un impact sur les performances de l’application. Bien que Nest essaie de mettre en cache autant de métadonnées que possible, il devra toujours créer une instance de votre classe à chaque requête. Par conséquent, cela ralentira votre temps de réponse moyen et le résultat global de l’évaluation. À moins qu’un fournisseur ne doive être à portée de requête, il est fortement recommandé d’utiliser la portée singleton par défaut.

Fournisseurs durables

Les fournisseurs à portée de requête, comme mentionné dans la section ci-dessus, peuvent entraîner une latence accrue, car avoir au moins 1 fournisseur à portée de requête (injecté dans l’instance du contrôleur, ou plus profondément - injecté dans l’un de ses fournisseurs) rend également le contrôleur à portée de requête. Cela signifie qu’il doit être recréé (instancié) pour chaque requête individuelle (et être collecté par le ramasse-miettes par la suite). Cela signifie aussi que, pour 30 000 requêtes en parallèle, il y aura 30 000 instances éphémères du contrôleur (et de ses fournisseurs à portée de requête).

Avoir un fournisseur commun dont la plupart des fournisseurs dépendent (pensez à une connexion de base de données, ou à un service de journalisation), convertit automatiquement tous ces fournisseurs en fournisseurs à portée de requête également. Cela peut poser un défi dans les applications multi-locataires, surtout pour celles qui ont un fournisseur “source de données” à portée de requête central qui récupère des en-têtes/tokens à partir de l’objet de requête et en fonction de ses valeurs, récupère la connexion/schema de base de données correspondante (spécifique à ce locataire).

Supposons que vous ayez une application utilisée alternativement par 10 clients différents. Chaque client a sa propre source de données dédiée, et vous voulez vous assurer que le client A ne pourra jamais accéder à la base de données du client B. Une façon d’y parvenir pourrait être de déclarer un fournisseur “source de données” à portée de requête qui, en fonction de l’objet de requête, détermine quel est le “client actuel” et récupère sa base de données correspondante. Avec cette approche, vous pouvez transformer votre application en une application multi-locataire en quelques minutes. Mais, un inconvénient majeur de cette approche est que, très probablement, une grande partie des composants de votre application repose sur le fournisseur “source de données”, ils deviendront implicitement “à portée de requête”, et par conséquent, vous verrez indubitablement un impact sur les performances de votre application.

Mais que se passerait-il si nous avions une meilleure solution ? Étant donné que nous n’avons que 10 clients, ne pourrions-nous pas avoir 10 sous-arbres DI individuels par client (au lieu de recréer chaque arbre par requête) ? Si vos fournisseurs ne dépendent d’aucune propriété qui serait vraiment unique pour chaque requête consécutive (par exemple, l’UUID de la requête) mais plutôt qu’il y a des attributs spécifiques qui nous permettent de les agréger (classifier), il n’y a aucune raison de recréer le sous-arbre DI à chaque nouvelle requête.

Et c’est exactement à ce moment que les fournisseurs durables entrent en jeu.

Avant de commencer à marquer les fournisseurs comme durables, nous devons d’abord enregistrer une stratégie qui instructe Nest sur ce que sont ces “attributs de requête communs”, fournir une logique qui groupe les requêtes - les associe à leurs sous-arbres DI correspondants.

Stratégie d'agrégation par ID de locataire
import { HostComponentInfo, ContextId, ContextIdFactory, ContextIdStrategy } from '@nestjs/core';
import { Request } from 'express';
const tenants = new Map<string, ContextId>();
export class AggregateByTenantContextIdStrategy implements ContextIdStrategy {
attach(contextId: ContextId, request: Request) {
const tenantId = request.headers['x-tenant-id'] as string;
let tenantSubTreeId: ContextId;
if (tenants.has(tenantId)) {
tenantSubTreeId = tenants.get(tenantId);
} else {
tenantSubTreeId = ContextIdFactory.create();
tenants.set(tenantId, tenantSubTreeId);
}
// If tree is not durable, return the original "contextId" object
return (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId;
}
}

La valeur renvoyée par la méthode attach instruit Nest sur quel identifiant de contexte doit être utilisé pour un hôte donné. Dans ce cas, nous avons spécifié que le tenantSubTreeId devrait être utilisé au lieu de l’objet contextId généré automatiquement, lorsque le composant hôte (par exemple, le contrôleur à portée de requête) est marqué comme durable. De plus, dans l’exemple ci-dessus, aucun payload ne serait enregistré (où payload = REQUEST/CONTEXT représentant le “racine” - parent du sous-arbre).

Si vous souhaitez enregistrer le payload pour un arbre durable, utilisez la construction suivante à la place :

Enregistrement de payload pour un arbre durable
return {
resolve: (info: HostComponentInfo) =>
info.isTreeDurable ? tenantSubTreeId : contextId,
payload: { tenantId },
}

Maintenant, chaque fois que vous injectez le fournisseur REQUEST (ou CONTEXT pour les applications GraphQL) en utilisant @Inject(REQUEST)/@Inject(CONTEXT), l’objet payload serait injecté (composé d’une seule propriété - tenantId dans ce cas).

Ainsi, avec cette stratégie en place, vous pouvez l’enregistrer quelque part dans votre code (puisqu’elle s’applique de toute façon globalement), par exemple, vous pourriez la placer dans le fichier main.ts :

Application de la stratégie d'agrégation
ContextIdFactory.apply(new AggregateByTenantContextIdStrategy());

Tant que l’enregistrement se produit avant qu’une requête n’atteigne votre application, tout fonctionnera comme prévu.

Enfin, pour transformer un fournisseur régulier en un fournisseur durable, il suffit de définir le drapeau durable sur true et de modifier sa portée en Scope.REQUEST (non nécessaire si la portée REQUEST est déjà dans la chaîne d’injection) :

Déclaration d'un fournisseur durable
import { Injectable, Scope } from '@nestjs/common';
@Injectable({ scope: Scope.REQUEST, durable: true })
export class CatsService {}

De même, pour les fournisseurs personnalisés, définissez la propriété durable dans la forme longue pour l’enregistrement d’un fournisseur :

Enregistrement d'un fournisseur personnalisé durable
{
provide: 'foobar',
useFactory: () => { ... },
scope: Scope.REQUEST,
durable: true,
}