Laravel

Le seeder qui recrée ce que vous avez renommé

`firstOrCreate` rend un seeder rejouable — tant que la clé de recherche ne bouge pas. Le jour où quelqu'un renomme depuis l'administration, le seeder ne reconnaît plus rien et réinsère l'ancienne fiche à côté de la nouvelle.

A Azetria Agence digitale 5 min de lecture
La base contient ecommerce-01, l'administration le renomme boutique-01, et le seeder relancé réinsère ecommerce-01 en second — suivi des quatre parades : clé immuable, test de réconciliation, ne pas écraser, décider qui possède la donnée
La base contient ecommerce-01, l'administration le renomme boutique-01, et le seeder relancé réinsère ecommerce-01 en second — suivi des quatre parades : clé immuable, test de réconciliation, ne pas écraser, décider qui possède la donnée
En bref
Un seeder écrit avec `firstOrCreate(['slug' => …])` n'est idempotent que par rapport au slug. Si l'application permet de modifier ce slug depuis une administration, le seeder ne retrouve plus l'enregistrement au passage suivant et en crée un second, publié, avec les valeurs d'origine. Les tests ne le détectent jamais, parce qu'ils tournent sur une base neuve où personne n'a rien renommé. La correction consiste à chercher sur une clé que l'application ne laisse pas modifier, ou à cesser de semer des données dont l'administration est devenue propriétaire.

Nous avons trouvé celui-là dans notre propre code, lundi, en ajoutant une image à une fiche de notre catalogue de sites. Rien n’était cassé. Rien ne se serait vu avant le prochain déploiement.

Le seeder du catalogue déclare quatre sites, chacun sous une clé qui sert de slug. Un de ces sites avait été renommé quelques jours plus tôt depuis l’administration : ecommerce-01 était devenu boutique-01, parce que c’est le mot que les clients emploient. Le code, lui, disait toujours ecommerce-01.

Ce qui allait se passer

Le seeder est écrit comme on les écrit tous, pour être rejouable sans dégât :

foreach ($definitions as $slug => $definition) {
    $modele = ModeleDeSite::firstOrCreate(
        ['slug' => $slug],
        [ /* nom, prix, description, statut… */ ],
    );
}

firstOrCreate cherche une ligne dont le slug vaut ecommerce-01. Elle n’existe plus : elle s’appelle boutique-01 depuis le renommage. La méthode fait donc ce pour quoi elle est écrite — elle crée. Au premier db:seed suivant, un cinquième site serait apparu au catalogue, publié, au même prix, avec la même description, à côté de celui que le client voit déjà.

Le mot « idempotent » est celui qui trompe. firstOrCreate est idempotent par rapport à sa clé de recherche, pas par rapport à l’enregistrement. Tant que la clé est immuable, les deux reviennent au même. Dès que l’application permet de la modifier, ce sont deux choses différentes — et c’est justement une administration qui permet de la modifier.

Pourquoi aucun test ne l’attrape

C’est la partie intéressante, et elle rappelle un autre piège que nous avions décrit ici : celui des N+1 que les tests ne voient pas. Le mécanisme est le même.

Les tests tournent sur une base neuve. On la migre, on la sème, on vérifie, on la jette. Dans cette base, personne n’a jamais rien renommé : les slugs en base sont exactement ceux du seeder, donc firstOrCreate retrouve tout, donc rien ne double. Le test est vert, et il a raison de l’être : sur les données qu’il possède, le code est correct.

Le bug n’existe que dans une base qui a une histoire. C’est la définition même de la production, et c’est la seule base que les tests ne rejouent jamais.

Trois façons de s’en sortir

Chercher sur une clé que personne ne peut modifier. La plus solide. On ajoute une colonne technique — reference, code, appelez-la comme vous voulez — qui n’apparaît nulle part dans l’interface et que rien ne permet de changer. Le seeder cherche sur elle, le slug devient un simple attribut, libre de bouger autant que le marketing le souhaite.

ModeleDeSite::firstOrCreate(
    ['reference' => $definition['reference']],   // jamais éditable
    [...$definition, 'slug' => $definition['slug']],
);

Réconcilier, et échouer bruyamment. Un test compare l’ensemble des références connues du seeder à celles présentes en base. Une fiche en base que le seeder ignore, ou l’inverse, et la suite échoue avec un message qui nomme le coupable. Cela ne corrige rien, mais cela transforme un doublon découvert par un client en une ligne rouge découverte avant le déploiement.

Cesser de semer. La plus radicale, et souvent la bonne. Un seeder a un rôle légitime : poser les données de référence, celles que personne ne modifie — rôles, permissions, taux de taxe, statuts de commande. Le jour où le client édite lui-même les fiches, elles ne sont plus des données de référence : elles sont son contenu. Continuer à les semer, c’est maintenir deux propriétaires pour la même ligne.

updateOrCreate ne sauve personne

C’est le premier réflexe, et il aggrave la situation.

Il cherche sur la même clé : un enregistrement renommé lui échappe exactement de la même façon, et le doublon apparaît quand même. En revanche, sur tous les enregistrements qu’il retrouve, il écrase — à chaque déploiement, il repose les valeurs du code par-dessus celles de l’administration. Le client corrige une faute dans un texte le mardi, elle disparaît au déploiement du jeudi, et personne ne comprend pourquoi.

Il existe un cas où updateOrCreate est le bon outil : quand le code est délibérément la source de vérité et que l’administration ne fait que consulter. Ce cas est plus rare qu’on ne le croit, et il mérite d’être écrit en commentaire au-dessus de l’appel, sans quoi quelqu’un ouvrira un jour l’édition de ces champs sans se douter de ce qu’il déclenche.

La question à se poser, une fois

Qui possède cette ligne : le dépôt, ou l’administration ?

La réponse tient en une phrase par table, et elle vaut d’être écrite quelque part. C’est le même genre de décision que celles qui font qu’une architecture tient encore après cinq ans : elle ne coûte rien à prendre au départ, et elle coûte un après-midi de rattrapage quand on ne l’a jamais prise.

Une ligne possédée par les deux finit toujours par diverger. La seule question est de savoir si vous l’apprendrez par un test, ou par un client qui vous demande pourquoi il y a deux fois la même chose sur son site.


Notre correction a tenu en deux lignes : le slug du seeder aligné sur celui de la production, et cinq lignes de commentaire au-dessus de la boucle pour expliquer le piège au prochain qui renommera quelque chose. La colonne immuable viendra ensuite, quand le catalogue comptera assez de fiches pour que le renommage devienne une habitude.

Ce n’est pas la correction la plus élégante. C’est celle qui empêche le doublon d’arriver ce soir.

Questions fréquentes

Non, et il en ajoute un. Il cherche sur la même clé, donc il ne retrouve pas davantage un enregistrement renommé : le doublon apparaît quand même. Et sur les enregistrements qu'il retrouve, il écrase les modifications faites en administration à chaque déploiement.

Un test qui compare l'ensemble des clés présentes en base à celles que déclare le seeder. S'il existe en base une fiche que le seeder ne connaît pas, ou l'inverse, le test échoue et vous l'apprenez avant le déploiement plutôt qu'en regardant la page publique.

Non, mais il faut savoir ce qu'ils sèment. Les données de référence que personne ne modifie — rôles, permissions, taux de taxe, statuts — restent parfaitement à leur place. Les contenus que le client édite lui-même n'y sont plus à leur place dès la première modification.

Corriger d'abord le seeder pour qu'il cesse d'en produire, puis supprimer la fiche en trop en vérifiant ce qui la référence. Une fiche publiée depuis quelques jours peut déjà être indexée : dans ce cas, une redirection permanente vaut mieux qu'une page qui disparaît.

A

Azetria

Agence digitale

Publications collectives de l'équipe Azetria : notes de veille, retours d'expérience de projet et synthèses méthodologiques.

  • Laravel
  • SaaS
  • IA appliquée
  • SEO

La prestation associée

Pour les entreprises dont le site doit faire plus qu'exister

Création de site web sur mesure avec Laravel

À lire ensuite

Votre projet mérite mieux qu'un devis générique

Dites-nous ce que vous voulez obtenir. Nous vous répondons avec une analyse, une fourchette et les questions que personne d'autre ne vous aura posées.

Réponse sous 4 heures ouvrées · Aucun engagement · Vos données restent chez nous