Opérations multi-tables avec l'ORM Django : requêtes directes et inverses, modèle intermédiaire et génération inverse

Remarques importantes sur l'ORM

  • Le champ id (AutoField) est ajouté automatiquement ; pour un nom personnalisé, déclarez-le explicitement.
  • Pour une clé étrangère, Django ajoute suffixe _id dans la base de données.
  • null=True sur une ForeignKey permet de stocker NULL.

Paramètres de ForeignKey

  • to : modèle lié ; 'self' pour auto-référence (relations hiérarchiques).
  • to_field : champ cible dans le modèle lié.
  • related_name : nom pour l'accès inverse depuis le modèle lié.
  • related_query_name : préfixe de requête inverse (remplace le nom de table dans les filtres).
  • db_constraint : si False, supprime la contrainte d'intégrité référentielle en base (la cohérence est à la charge de l'application).

Exemple d'utilisation de related_name et related_query_name

class Auteur(models.Model):
    nom = models.CharField(max_length=100)

class Livre(models.Model):
    titre = models.CharField(max_length=100)
    auteur = models.ForeignKey(Auteur, on_delete=models.CASCADE, related_name='livres')

# Accès inverse via related_name
auteur = Auteur.objects.get(id=1)
livres = auteur.livres.all()  # équivaut à Livre.objects.filter(auteur=auteur)

# Filtre avec related_query_name
auteurs = Auteur.objects.filter(livre__titre='Django débutant')  # 'livre' est le related_query_name par défaut

Relations multi-tables

Un-à-un (OneToOneField)

Crée une clé étrangère avec unique=True.

class ProfilAuteur(models.Model):
    adresse = models.CharField(max_length=100)
    telephone = models.CharField(max_length=20)
    auteur = models.OneToOneField('Auteur', on_delete=models.CASCADE)

Un-à-plusieurs (ForeignKey)

La clé étrangère est placée du côté « plusieurs ».

class MaisonEdition(models.Model):
    nom = models.CharField(max_length=100)

class Ouvrage(models.Model):
    titre = models.CharField(max_length=100)
    editeur = models.ForeignKey(MaisonEdition, on_delete=models.CASCADE)

Plusieurs-à-plusieurs (ManyToManyField)

Django crée automatiquement une table de liaison.

class Etudiant(models.Model):
    nom = models.CharField(max_length=100)
    cours = models.ManyToManyField('Cours')

Opérations d'ajout et de modification

Ajout en relation un-à-plusieurs

# Méthode 1 : affectation directe de id
Livre.objects.create(titre='Python avancé', editeur_id=2)

# Méthode 2 : affectation d'objet
editeur = MaisonEdition.objects.get(nom='O\'Reilly')
Livre.objects.create(titre='Flask pour débutants', editeur=editeur)

Ajout en relation plusieurs-à-plusieurs

etudiant = Etudiant.objects.get(nom='Alice')
c1 = Cours.objects.get(titre='Maths')
c2 = Cours.objects.get(titre='Physique')
etudiant.cours.add(c1, c2)          # avec objets
etudiant.cours.add(3, 4)            # avec ids
etudiant.cours.set([5, 6])          # remplace toutes les relations
etudiant.cours.remove(c1)           # supprime une relation
etudiant.cours.clear()              # vide toutes les relations

Requêtes inter-tables (objets et champs)

Requête directe (du côté où se trouve la clé étrangère)

# Objet
livre = Livre.objects.get(titre='Python')
print(livre.editeur.nom)

# Champ (double underscore)
livres = Livre.objects.filter(editeur__nom='O\'Reilly')

Requête inverse (depuis le modèle non porteur de la clé)

# Objet : nom_manager_set (ou related_name)
editeur = MaisonEdition.objects.get(nom='O\'Reilly')
livres = editeur.livre_set.all()  # 'livre' est le nom du modèle en minuscule

# Champ : nom_modele__champ
editeurs = MaisonEdition.objects.filter(livre__titre='Python')

Cas particulier un-à-un : pas de _set, on utilise directement le nom du modèle (p. ex. profil.auteur.nom).

Requêtes chaînées sur plusieurs tables

# Obtenir les titres des livres et les noms des éditeurs
# dont l'auteur habite une ville commençant par 'Lyon'
resultats = Livre.objects.filter(
    auteur__profil__ville__startswith='Lyon'
).values('titre', 'editeur__nom')

Modèle intermédiaire (through)

Lorsque la table de liaison doit contenir des champs supplémentaires, on définit un modèle explicite et on l'indique avec through.

class Membre(models.Model):
    nom = models.CharField(max_length=100)

class Club(models.Model):
    nom = models.CharField(max_length=100)
    membres = models.ManyToManyField(Membre, through='Adhesion')

class Adhesion(models.Model):
    membre = models.ForeignKey(Membre, on_delete=models.CASCADE)
    club = models.ForeignKey(Club, on_delete=models.CASCADE)
    date_inscription = models.DateField()
    motivation = models.CharField(max_length=200)

Avec un modèle intermédiaire, les méthodes add(), create(), set() sont interdites. On doit créer directement des instances du modèle intermédiaire.

# Création
Adhesion.objects.create(membre=m1, club=c1, date_inscription=date.today(), motivation='Passion')

# Nettoyage
club.membres.clear()  # supprime toutes les Adhesion liées

Opérations en masse (bulk_create / bulk_update)

# Insertion en masse
nouvelles_adhesions = [
    Adhesion(membre=m, club=c, date_inscription=date.today(), motivation='Découverte')
    for m in membres
]
Adhesion.objects.bulk_create(nouvelles_adhesions)

# Mise à jour en masse : supprimer puis insérer
with transaction.atomic():
    Adhesion.objects.filter(club=c).delete()
    Adhesion.objects.bulk_create(nouvelles_adhesions)

Génération inverse de modèles (inspectdb)

Django peut générer des classes de modèles à partir d'une base de données existante.

python manage.py inspectdb > models.py

La commande inspectdb produit un fichier models.py contenant les définitions crorespondant aux tables de la base. On peut ensuite l'importer dans une application Django.

Étiquettes: Django ORM ForeignKey OneToOneField ManyToManyField

Publié le 14 juin à 23h27