Relier les fixtures entre elles¶
Objectif : produire un jeu de données réaliste où les tables se référencent, un eleve rattaché à un compte users, sans jamais coder d'Id en dur.
Ce que vous allez apprendre : les colonnes réelles de vos factories, la référence à une autre table par une clé naturelle, et l'ordre de chargement qui respecte les clés étrangères.
Préparer les deux tables¶
Ce palier relie deux tables, il faut donc que les deux existent.
forge make:auth
forge auth:init
forge make:entity Eleve --field "nom:string:max_length=80" --field "prenom:string:max_length=80" --field "user_id:integer"
forge db:apply
make:auth puis auth:init posent la table users de l'authentification, celle que l'élève va référencer.
make:entity déclare l'entité Eleve avec sa colonne user_id, et db:apply crée les tables en base.
Le champ user_id est ici un entier ordinaire, pas un champ foreign_key.
La distinction se voit dans le nom de colonne, et la section suivante l'explique.
Les colonnes sont celles de la table¶
Une factory produit un dict dont les clés sont les colonnes réelles de la table, pas les noms de champs du contrat.
Un champ nom devient la colonne Nom, un champ user_id la colonne UserId ; une clé étrangère déclarée en foreign_key garde son nom snake (user_id).
C'est exactement ce que fixtures:make-factory échafaude pour vous :
Le SQL généré porte donc les bonnes colonnes, et tourne tel quel sur votre backend (MariaDB comme PostgreSQL).
Référencer une autre table¶
Un eleve pointe un compte users. Au moment de générer les fixtures, l'Id du compte n'existe pas encore : il sera attribué par la base au chargement.
On référence donc la ligne par une clé naturelle (un email, un identifiant unique), pas par son Id :
from forge_mvc_fixtures import Factory
class EleveFactory(Factory):
table = "eleve"
def rows(self, count: int) -> list[dict]:
return [{
"Nom": self.faker.last_name(),
"Prenom": self.faker.first_name(),
"UserId": self.reference("users", "Email", "prof.durand@ecole.fr"),
} for _ in range(count)]
self.reference("users", "Email", "prof.durand@ecole.fr") ne renvoie pas un nombre : c'est une référence que la génération traduit en sous-requête SQL.
Le SQL reste visible¶
Générez, puis lisez le .sql :
La référence est rendue en sous-requête, résolue au chargement contre le vrai Id :
INSERT INTO eleve (Nom, Prenom, UserId)
VALUES ('Durand', 'Hélène', (SELECT Id FROM users WHERE Email = 'prof.durand@ecole.fr' LIMIT 1));
Rien de caché : vous voyez la sous-requête, vous la relisez, vous la comprenez.
L'ordre de chargement suit les clés étrangères¶
Un compte users doit exister avant l'eleve qui le référence.
fixtures:load s'en charge : il ordonne les fichiers par tri topologique de leurs dépendances : les clés étrangères déclarées dans mvc/entities/relations.json, mais aussi les liens reference() eux-mêmes.
Ainsi une factory qui pose reference("users", …) est chargée après ce qui fournit users, même si users n'est pas une entité mvc/entities/.
La table users est donc chargée avant eleve, même si le nom de fichier eleve.sql vient avant users.sql dans l'alphabet.
Si un jeu n'est pas triable (une dépendance circulaire), l'option --no-fk-checks désactive les contraintes le temps du chargement, puis les réactive.
À réserver aux cas où l'ordre ne suffit pas : par défaut, l'ordre topologique respecte l'intégrité, ce qui est préférable.
Les timestamps sont posés pour vous¶
Beaucoup d'entités déclarent options.timestamps: true : les colonnes CreatedAt et UpdatedAt sont NOT NULL, sans valeur par défaut en base.
Vous n'avez pas à les écrire dans chaque factory : fixtures:generate lit le contrat de l'entité et les ajoute automatiquement aux INSERT, avec un horodatage constant (fixtures reproductibles).
Si vous fournissez vous-même CreatedAt dans la factory, c'est votre valeur qui est gardée ; une entité sans timestamps n'est pas concernée.
Ce qu'il faut retenir¶
- les clés du dict sont les colonnes réelles de la table ;
self.reference(table, colonne, valeur)relie une ligne à une autre par une clé naturelle, sansIden dur ;fixtures:loadcharge dans l'ordre des dépendances ;--no-fk-checksreste une échappatoire pour les cycles ;- les timestamps
NOT NULL(CreatedAt,UpdatedAt) sont ajoutés automatiquement à la génération.
Tout cela reste des données statiques (des .sql). Le palier suivant montre comment exécuter du code Python pour ce que le SQL ne peut pas exprimer.
La suite¶
Voyons comment importer un référentiel ou calculer des valeurs, avec une fixture callable.