Traiter de larges jeux de données avec un itérateur Doctrine

Publié le 16/11/2019 • Actualisé le 27/11/2019


English language detected! 🇬🇧

  We noticed that your browser is using English. Do you want to read this post in this language?

Read the english version 🇬🇧 Close

Quand on a un grand nombre de lignes à traiter avec Doctrine, ce n'est pas une bonne idée d'utiliser les fonctions classiques comme getResult() parce que vous allez rapidement rencontrer des problèmes de mémoire. C'est là qu'intervient la fonction iterate. Elle retourne un objet IterableResult sur lequel on peut boucler sans avoir à être effrayé par ceux-ci. Voici un exemple. Évidemment, sur ce blog, je n'ai pas des millions de de lignes... bientôt peut-être ! 😉
J'ai testé recemment ce code pour la migration d'une table vers une autre avec des règles fonctionnelles. Il y avait un demi million de lignes à traiter. Le traitement a consommé environ 250mo de mémoire et a pris cinq minutes pour finir.
PS : N'oubliez pas de désactiver le mode debug !


<?php

declare(strict_types=1);

namespace App\Controller\Snippet;

use App\Entity\Article;
use App\Repository\ArticleRepository;
use Doctrine\ORM\EntityManagerInterface;

/**
 * J'utilise un trait PHP afin d'isoler chaque snippet dans un fichier.
 * Ce code doit être apellé d'un contrôleur Symfony étendant AbstractController (depuis Symfony 4.2)
 * ou Symfony\Bundle\FrameworkBundle\Controller\Controller (Symfony <= 4.1).
 * Les services sont injectés dans le constructeur du contrôleur principal.
 *
 * @property ArticleRepository      $articleRepo
 * @property EntityManagerInterface $entityManager
 */
trait Snippet56Trait
{
    public function snippet56(): void
    {
        $qb = $this->articleRepo->createQueryBuilder('get_all_articles');
        $processed = [];
        $batchSize = 3;
        $flushCount = 0;

        echo 'Memory before: '.round(memory_get_usage() / 1024 / 1024, 2)." mb\n";

        // using getQuery()->iterate() is deprecated. It's better now because we
        // directly get the entity object as the loop item.
        foreach ($qb->getQuery()->toIterable() as $article) {
            // $article = $result[0] ?? null; // with iterate() we had to do this to get the Article object
            if (!$article instanceof Article) {
                throw new \TypeError('Invalid article object found.');
            }

            if ((\count($processed) % $batchSize) === 0) {
                // persist here
                $this->entityManager->flush();
                $this->entityManager->clear(); // call clear, so memory can be freed.
                ++$flushCount;
            }
            $processed[] = $article->getId();
        }
        $this->entityManager->flush(); // for last rows
        $this->entityManager->clear();

        echo 'Memory after: '.round(memory_get_usage() / 1024 / 1024, 2).' mb'.PHP_EOL;
        echo 'Number of flush/clear: '.($flushCount + 1).PHP_EOL;
        echo 'Processed articles with IDs: '.implode(',', $processed);

        // That's it! 😁
    }
}

 Exécuter le snippet  Plus sur Stackoverflow   Lire la doc  Snippet aléatoire

  Travaillez avec moi !