Gestion de la volumétrie¶
Pour gérer tout type de volumétrie l'API utilise 2 stratégies :
- le chargement lazy des données
- la pagination des appels HTTP
Chargement lazy des données¶
Cette opération consiste à ne charger qu'une entité à la fois depuis la base de données ce qui permet de conserver un usage constant de la mémoire. Pour effectuer cette opération on utilise le service Innmind\Doctrine\Manager et l'usage ressemble toujours à ça :
function foo(Manager $manager) {
$entities = $manager
->repository(Entity::class)
->all()
->lazy()
->fetch();
}
Note: l'appel à ->all() peut être remplacé par ->matching() pour filtrer les entités que l'on souhaite récupérer.
Warning: ->lazy() ne peut pas fonctionner en utilisant ->matching() si une des comparaisons est faite sur une relation en one to many ou many to many.
L'appel de la méthode ->fetch() nous retourne un object Innmind\Immutable\Sequence basé sur un Generator qui nous permet de ne traiter qu'une entité à la fois.
L'appel à la méthode ->lazy() indique à la Sequence de se débarasser du Generator une fois qu'on a parcouru toutes les entités, cela implique que si on redemande à itérer sur la même Sequence un nouvel appel à la base de données est effectuée. Dans un contexte d'écriture où on veut pouvoir effectuer plusieurs opérations sur une même Sequence cela pose problème, pour éviter ces requêtes il ne faut pas utiliser ->lazy(), ou à defaut il faudra appeler la méthode ->memoize() pour forcer à conserver toutes les entités en mémoire.
Cependant dû au fonctionnement de Doctrine une fois qu'une entité est chargée en mémoire elle y reste ad-vitam. Lorsqu'on veut donc traiter une Sequence avec beaucoup d'éléments il faut obligatoirement appeler la méthode $manager->clear() pour libérer les entités stockées en mémoire par Doctrine. Mais pour que cette technique fonctionne il est impératif que tout traitement sur une entité soit fini avant de ->clear() sinon on risque de manipuler des entités dont Doctrine n'a plus connaissance et il n'arrivera pas à charger les relations.
Ces traitements ressembleront toujours à :
function foo(Manager $manager) {
$_ = $manager
->repository(Entity::class)
->all()
->lazy()
->fetch()
->foreach(function($entity) use ($manager) {
doStuff($entity);
$manager->clear();
});
}
Dans un controller une partie de cette logique peut être masquée en utilisant cette stratégie :
use App\Infrastructure\Http\Stream;
use Symfony\Component\HttpFoundation\Response;
final class Controller
{
public function action(Stream $stream, Entity\Repository $repository): Response
{
return $stream(
$repository
->all()
->lazy()
->map(static fn($entity) => $entity->expose()),
)->toResponse();
}
}
C'est le service Stream qui a pour responsabilité de faire le clear de Doctrine.
Pagination des appels HTTP¶
Avec la stratégie décrite au dessus on a la possibilité de streamer de longues listes au format JSON mais l'appelant de notre API ne supporte pas forcément cette volumétrie, ce qui l'obligerait à stocker toutes les données en mémoire.
Pour éviter ce problème à nos clients on supporte l'usage du header HTTP Range qui permet de spécifier le nombre de ressources qu'il souhaite récupérer. En spécifiant le header Range: resources 0-99 il demande les 100 premières entités et la réponse contiendra un header Content-Range: 0-99/500 où 500 indique le nombre de ressources totales disponibles.
Cependant cette approche pourrait être problématique du point de vue de l'API car l'utilisateur pourrait demander trop de ressources qui ne tiendraient pas en mémoire, mais en utilisant le streaming du JSON on s'absout de ce problème.