Lire les commandes par lots depuis la base
Objectif : Parcourir un grand jeu de commandes sans conserver tous les enregistrements en mémoire.
Un export de plusieurs millions de commandes ne doit pas commencer par une requête qui charge toutes les lignes en mémoire. Cette approche peut ralentir le service ou épuiser la mémoire disponible. Vous allez définir une lecture par lots qui ne conserve que la page en cours, puis transmet chaque commande au traitement appelant.
Dans les leçons précédentes, le service de facturation a été séparé de son infrastructure par injection de dépendances. Vous reprenez ici la même approche : le service d’export reçoit un dépôt qui sait lire un lot de commandes. La connexion à la base et la requête propre à votre moteur restent dans l’adaptateur de dépôt déjà utilisé par la composition de l’application.
Pourquoi lire par lots
Une lecture globale, par exemple depot.listerToutesLesCommandes(), crée une collection dont la taille croît avec le nombre de lignes. Une lecture par lots limite au contraire chaque résultat à une taille connue : le service demande un lot, le traite, puis demande le suivant. La mémoire utilisée pour les résultats reste ainsi liée à la taille du lot, plutôt qu’au volume total de commandes.
Pour retrouver la suite, chaque lot doit être parcouru dans un ordre stable. Ici, les commandes sont ordonnées par identifiant croissant et l’identifiant de la dernière commande lue devient le curseur. Le lot suivant ne contient que les commandes dont l’identifiant est supérieur à ce curseur, ce qui évite de recalculer un décalage de lignes de plus en plus coûteux sur une table volumineuse.
Définir le parcours dans le service d’export
Le fichier src/services/exports.js porte la logique de parcours, sans dépendre d’une bibliothèque de base de données. Son contrat est explicite : lireLot reçoit un curseur et une limite, et renvoie un tableau de commandes ordonné par identifiant croissant. La fonction appelée pour chaque commande est injectée elle aussi ; vous pourrez la remplacer ou la composer avec la transformation en flux étudiée dans les leçons suivantes.
'use strict';
function creerServiceExports({ depot, tailleLot }) {
if (!depot || typeof depot.lireLot !== 'function') {
throw new TypeError('Le dépôt doit fournir lireLot.');
}
if (!Number.isInteger(tailleLot) || tailleLot < 1) {
throw new TypeError('La taille du lot doit être un entier positif.');
}
async function parcourirCommandes(traiterCommande) {
if (typeof traiterCommande !== 'function') {
throw new TypeError('Le traitement des commandes doit être une fonction.');
}
let curseur = null;
while (true) {
const commandes = await depot.lireLot({
apresId: curseur,
limite: tailleLot
});
if (!Array.isArray(commandes)) {
throw new TypeError('Le dépôt doit renvoyer un tableau de commandes.');
}
if (commandes.length === 0) {
return;
}
for (const commande of commandes) {
if (commande.id === undefined || commande.id === null) {
throw new TypeError('Chaque commande doit avoir un identifiant.');
}
await traiterCommande(commande);
curseur = commande.id;
}
}
}
return { parcourirCommandes };
}
module.exports = { creerServiceExports };Le curseur progresse après le traitement de chaque commande, et non avant. Si le traitement échoue, l’erreur remonte et le parcours s’interrompt au lieu de prétendre que l’export est terminé. Le prochain lot ne peut donc être demandé qu’après le traitement du lot courant ; cette séquence maintient une consommation bornée et prévisible.
Vérifier le parcours avec un dépôt simulé
Ce test autonome exécute le service avec trois commandes et une taille de lot de deux. Le dépôt respecte le contrat en filtrant sur le curseur, en triant les identifiants et en limitant le résultat ; il simule ainsi le comportement que devra fournir votre adaptateur de base de données.
'use strict';
const { creerServiceExports } = require('./src/services/exports');
const donnees = [
{ id: 1, total: 1200 },
{ id: 2, total: 850 },
{ id: 3, total: 2400 }
];
const depot = {
async lireLot({ apresId, limite }) {
return donnees
.filter((commande) => apresId === null || commande.id > apresId)
.sort((a, b) => a.id - b.id)
.slice(0, limite);
}
};
async function executer() {
const service = creerServiceExports({ depot, tailleLot: 2 });
const identifiants = [];
await service.parcourirCommandes(async (commande) => {
identifiants.push(commande.id);
});
console.log(identifiants.join(', '));
}
executer().catch((erreur) => {
console.error(erreur);
process.exitCode = 1;
});1, 2, 3Le résultat confirme que toutes les commandes sont visitées dans le même ordre, même si le dernier lot est incomplet. Pour une base réelle, implémentez lireLot avec un filtre strictement supérieur au curseur, un tri explicite sur la même clé et une limite égale à limite. La requête doit renvoyer au plus cette quantité de lignes ; le service n’a pas besoin de connaître le moteur utilisé.
À retenir
- Lisez les commandes par lots afin de borner la mémoire indépendamment du volume total.
- Utilisez un curseur fondé sur une clé unique et un ordre explicite et stable.
- Faites progresser le curseur après chaque commande traitée et propagez les erreurs.
- Injectez le dépôt et la taille de lot ; le service d’export ne dépend pas du moteur de base de données.
Dans la prochaine leçon, vous transformerez les commandes parcourues en données adaptées à un export, sans charger l’ensemble du résultat en mémoire.