EntityManager::clear() nettoie un peu trop bien

Aujourd’hui en développant, je suis tombé sur un cas de figure assez basique mais qui fait péter une grosse erreur à Doctrine.

Admettons que vous avez deux entités liées par un ManyToOne. On va reprendre l’exemple du précédent billet en disant que l’entité principale est Codeur et l’entité propriétaire de la relation est Pantalon. Vous codez une méthode permettant d’ajouter, d’un coup, plusieurs pantalons à un codeur (par exemple via un formulaire d’ajout de pantalon comportant un bouton « + »). Dans votre traitement vous obtiendrez quelque chose comme ceci (si vous êtes bons 😉 :

    public function ajouterPantalonsActions(){
        // admettons que vos pantalons sont déjà dans la variables $pantalons
        
        // vous récupérez votre codeur dans la base
        $em = $this->getDoctrine()
                   ->getManager();
        
        $codeur = $em->getRepository('PetegoreDemoBundle:Codeur')
                     ->findOneByUsername("pete_gore");
        
        // puis vous bouclez sur les pantalons pour les lui ajouter
        foreach ($pantalons as $pantalon) {
            $pantalon->setCodeur($codeur);
            $em->persist($pantalon);
        }
        $em->flush();
    }

Jusque là, tout va bien. Maintenant imaginez que vous devez ajouter 2 000 pantalons d’un coup (genre le mec vient d’acheter un magasin avec son salaire de codeur). Vous êtes consciencieux et prenez en exemple le paragraphe de Doctrine sur le Batch Processing. Vous modifiez donc votre boucle afin de faire flush + clear tous les 100 pantalons.

    public function ajouterPantalonsActions(){
        // admettons que vos pantalons sont déjà dans la variables $pantalons
        
        // vous récupérez votre codeur dans la base
        $em = $this->getDoctrine()
                   ->getManager();
        
        $codeur = $em->getRepository('PetegoreDemoBundle:Codeur')
                     ->findOneByUsername("pete_gore");
        
        // puis vous bouclez sur les pantalons pour les lui ajouter
        $buffer = 0;
        $maxBuffer = 100;
        foreach ($pantalons as $pantalon) {
            $pantalon->setCodeur($codeur);
            $em->persist($pantalon);
            $buffer++; 
            if ($buffer >= $maxBuffer){
                $em->flush();
                $em->clear();
                $buffer = 0;
            }
        }
        $em->flush();
        $em->clear();
    }

Et là, c’est le drame. Votre script vous balance une superbe erreur du genre :

A new entity was found through the relationship ‘Petegore\DemoBundle\Entity\Pantalon#codeur’ that was not configured to cascade persist operations for entity: pete_gore. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={« persist »}).

En fait, ce que Doctrine ne précise pas dans sa doc, c’est que sa solution fonctionne bien si l’on n’utilise aucune entité actuellement récupérée via l’EntityManager. Autrement dit : dans notre cas, le $codeur étant récupéré via l’EM, il est lié à celui-ci par référence. En faisant un clear(), on le « perd » ; de ce fait, au flush suivant, Doctrine nous dit « hey, je le connais pas moi, ce codeur !« .

La solution est très simple, bien qu’elle soit un peu crade : récupérer le user après chaque clear :

    public function ajouterPantalonsActions(){
        // admettons que vos pantalons sont déjà dans la variables $pantalons
        
        // vous récupérez votre codeur dans la base
        $em = $this->getDoctrine()
                   ->getManager();
        
        $codeur = $em->getRepository('PetegoreDemoBundle:Codeur')
                     ->findOneByUsername("pete_gore");
        
        // puis vous bouclez sur les pantalons pour les lui ajouter
        $buffer = 0;
        $maxBuffer = 100;
        foreach ($pantalons as $pantalon) {
            $pantalon->setCodeur($codeur);
            $em->persist($pantalon);
            $buffer++; 
            if ($buffer >= $maxBuffer){
                $em->flush();
                $em->clear();
                $buffer = 0;
                $codeur = $em->getRepository('PetegoreDemoBundle:Codeur')
                             ->findOneByUsername("pete_gore");
            }
        }
        $em->flush();
        $em->clear();
        // récupérez-le ici aussi si vous en avez encore besoin par la suite
    }

Je vous sens dubitatif, genre « c’est quoi ce problème bidon, Doctrine n’a pas pensé à ça ?!« . Essayez, vous verrez !