Files
training.django/documentation/06-orm-models.md
Steve Kossouho e3ebf6bf4f Add documentation and source
Added documentation, source and extra files.
2025-07-02 20:26:50 +02:00

39 KiB

from django.http import Http404--- title: Django author: Steve Kossouho

Persistance des données


ORM

Encore très fréquemment, les projets utilisant une base de données utilisent du SQL brut. Les raisons sont diverses : les développeurs sont plus à l'aise avec du SQL, le responsable de bases de données insiste pour que la partie métier de l'intelligence du projet repose sur du code écrit directement à l'intérieur de la base, ou encore le projet est très simple et ne nécessite pas plus que quelques requêtes.


Dans la pratique, chaque SGBDR propose systématiquement sa propre variation de la syntaxe SQL standard, qu'il s'agisse de PRAGMA{.sql}, de types non standard ou de fonctionnalités spécifiques. Si votre projet est un minimum complexe, la probabilité que votre SQL soit incompatible avec d'autres moteurs de base de données est très élevée. (grosso modo : vous êtes verrouillé)


ORM : concept

Les ORM, ou Object Relational Managers, sont une couche d'abstraction dans les langages objet pour gérer l'accès aux bases de données, généralement sans écrire de SQL. Ils nécessitent souvent une courbe d'apprentissage plus ou moins abrupte, en plus de connaître les fondamentaux des bases de données. En général, le « tarif d'entrée » demeure néanmoins très abordable.


ORM : avantages

Les ORM présentent des avantages non négligeables sur le SQL brut :

  • Largement agnostiques (pas d'a priori à part sur les fonctionnalités)
  • Confortables à utiliser
  • Systèmes de migrations (voir fin de chapitre)
  • Requêtes basées sur le langage (Python ici) au lieu de SQL

ORM de Django

L'ORM de Django est directement intégré au framework, et inutilisable sans un projet web spécifiquement écrit pour Django. (les raisons sont diverses, ex. système de configuration, détection du code etc.)

Cependant, cet ORM, étant intégré au framework, propose un système accessible et rapidement fonctionnel pour communiquer avec une base de données. Pour prendre en main ce système, il y a peu d'étapes principales à observer :

  1. Configurer les bases de données dans settings.DATABASES{.python}
  2. Ajouter une application dans settings.INSTALLED_APPS{.python}
  3. Rédiger des classes de modèles dans ladite application

Paramètres sur les bases de données

Première étape lorsque vous voulez utiliser des bases de données, configurer la ou les bases de données que vous souhaitez exploiter dans votre projet. Django propose une variable de paramètres, settings.DATABASES.

Cette variable est déjà définie par défaut pour utiliser SQLite, mais elle peut être configurée plus en détail, pour pouvoir gérer en même temps plus bases de données, de types différents qui plus est !

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': 'mydatabase',
    }
}

Selon l'interface (ENGINE) que vous choisissez, les options disponibles peuvent varier et sont documentées correctement sur le site officiel.


Définition des modèles de données

Django détecte automatiquement la définition de vos modèles de données en allant les chercher dans le module models des applications utilisées par votre projet. (Notes : Ledit module models peut aussi être défini sous la forme d'un package tant qu'il peut être importé et contient des définitions de modèle)


Pour définir un "modèle" de données, il faut, dans le module models de votre application, déclarer des classes héritant de django.db.models.Model :

from django.db import models


class MyModel(models.Model):
    champ1 = models.CharField(max_length=32)

Introduction sur les modèles


Champs des modèles

La définition d'une classe de données représente une table dans la base de données, et les champs de la table sont représentés par des attributs dans la classe de modèle, qui sont des instances de certains types spécifiques (CharField dans l'exemple précédent).

Voici quelques exemples de champs :

from django.db import models

class Subscriber(models.Model):
    name = models.CharField(max_length=32)  # caractères
    level = models.IntegerField(verbose_name="niveau")  # nombre entier signé
    is_active = models.BooleanField(default=True)  # vrai ou faux

Champs disponibles pour les modèles


Concepts des ORM

Objet Correspond à...
Classe de modèle Table de base de données
Attribut du modèle Colonne dans la table
Instance du modèle Enregistrement de la table

Arguments des champs de modèles

Les initialiseurs de champs possèdent tous des arguments, certains communs à la plupart des champs, certains obligatoires etc. (voir Référence des champs de modèle dans la documentation officielle)

Champ de Modèle Attributs Obligatoires
CharField max_length
IntegerField Aucun
BooleanField Aucun
ForeignKey to, on_delete
OneToOneField to, on_delete
ManyToManyField to

Arguments fréquemment utilisés

Arguments fréquemment utilisés Description
default Valeur par défaut
verbose_name Nom de champ
db_index Ajouter un index sur la base de données
null Valeur NULL
blank Valeur '' ou sélection facultative
unique Ajouter une contrainte unique

Exemple de définition de modèle

from django.db import models
from uuid import uuid4
from datetime import date

class Author(models.Model):
    uuid = models.UUIDField(default=uuid4, db_index=True, verbose_name="UUID")  
    first_name = models.CharField(max_length=64, blank=False, verbose_name="first name")
    last_name = models.CharField(max_length=64, blank=False, verbose_name="last name")
    description = models.TextField(blank=True, verbose_name="description")
    birth_date = models.DateField(default=date(2000, 1, 1), verbose_name="birth date")
    registration_date = models.DateTimeField(auto_now_add=True, verbose_name="registration date")

    class Meta:  # Django models metadata
        verbose_name = "author"
        verbose_name_plural = "authors"

Par défaut, chaque modèle contient automatiquement un champ id qui est la [clef primaire]{.naming} de la table.


Classe Meta des modèles (options)

Lorsque l'on définit des classes de modèles et des champs pour ces modèles, on peut se rendre compte qu'il n'y a pas de solution évidente pour exprimer des contraintes s'appliquant à plusieurs champs, par exemple.

Imaginez par exemple les [index sur plusieurs champs]{.naming}, l'[unicité sur plusieurs champs]{.naming}...

Django propose un système d'options pour nos classes de modèle, les [options Meta]{.naming}. Ce système permet de définir plusieurs informations sur la table associée au modèle. Pour les définir sur un modèle, vous devez déclarer une classe nommée Meta à l'interieur de votre modèle, et y ajouter des options sous la forme d'attributs dans cette classe.


from django.db import models


class Person(models.Model):
    first_name = models.CharField(max_length=32)
    last_name = models.CharField(max_length=32)

    class Meta:
        verbose_name = "personne"
        verbose_name_plural = "personnes"
        unique_together = [("first_name", "last_name")]

Vous pouvez déclarer de nombreux attributs à la classe Meta, tels que ceux décrits dans la documentation sur les options Meta. Une bonne pratique est de définir au moins les attributs verbose_name et verbose_name_plural, dont l'objectif est de fournir des noms décrivant les modèles au niveau de l'interface utilisateur.


Migrations

Avant la sortie de Django 1.7 (2016), la gestion des modèles et de la base de données était très simple; vous définissiez un modèle, et Django vous proposait une commande de gestion manage.py syncdb, qui envoyait une commande SQL de création de table, selon ce que proposait votre modèle.

Problème : Si votre modèle venait à évoluer après avoir lancé la commande manage.py syncdb, Django ne pouvait plus mettre à jour votre base de données avec les changements.

Django 1.7 introduit la gestion des migrations, où vous pouvez faire évoluer votre modèle et mettre à jour votre base de données de façon différentielle/cumulative.

Documentation sur les migrations


Concept des migrations

Une migration est un delta de modifiations à appliquer à une base de données pour que son schéma soit cohérent avec le code des modèles. Imaginons le scénario suivant :

Nous réalisons un site web avec des fiches de personnages pour un jeu. Nous avons une visibilité faible sur les objectifs à long terme, donc nous fabriquons notre contenu au fur et à mesure.

from django.db import models

class Character(models.Model):
    name = models.CharField(max_length=32)
    race = models.CharField(max_length=32)
    level = models.IntegerField()

Nous créons une première version du modèle Character, mais nous devons créer une migration avec la commande manage.py makemigrations avant que la base de données ne soit raccord avec notre modèle. La migration est un fichier qui indique à Django quelles modifications appliquer pour que la base corresponde au modèle.

Dans notre cas, une migration initiale pour l'application sera créée et contiendra des opérations pour créer la table Character. Nous modifierons la base avec la commande manage.py migrate, qui va appliquer notre migration.


Imaginons ensuite que nous nous rendions compte que nous souhaitons ajouter à notre modèle des informations telles que des points de vie ou de mana. Nous créons alors deux nouveaux champs :

from django.db import models

class Character(models.Model):
    name = models.CharField(max_length=32)
    race = models.CharField(max_length=32)
    level = models.IntegerField()
    hp = models.IntegerField(default=10)  # nouveau
    mp = models.IntegerField(default=0)  # nouveau

Si nous recréons une migration avec la commande manage.py makemigrations, nous obtenons un second fichier de migration contenant des opérations pour ajouter ces champs. Pour parvenir à créer une migration ou l'appliquer, il faudra souvent faire attention aux contraintes sur les champs, ie. un champ n'acceptant pas de valeurs nulles doit être créé avec une valeur par défaut, etc.


Conservation des migrations

Une fois que vous avez créé une migration, et que vous l'avez appliquée, vous devez la conserver dans votre application; en effet, quelqu'un qui souhaite utiliser votre application dans son projet doit mettre à jour sa base de données en appliquant les deltas de toutes les migrations.

Ce système permet aussi bien à des développeurs ne possédant pas l'application qu'à des développeurs en possédant une version ancienne d'introduire les nouveautés à leurs projets.


Interagir avec la base de données

Une fois que l'on a défini nos premières classes de modèles, créé des migrations, et appliqué ces dernières, on peut commencer à interroger la base pour récupérer des données ou en ajouter. Pour découvrir l'API de l'ORM de Django, nous pouvons lancer un shell avec la commande manage.py shell.

Malheureusement, ladite console oblige à importer ses modèles à chaque lancement (Django 5.2, prévu en avril 2025, introduira l'import automatique des modèles).

Jusqu'à Django 5.1 inclus, vous pouvez vous faciliter l'existence avec deux bibliothèques Python/Django :

Package Description
ipython shell Python avancé (autocompletion etc.)
django_extensions shell Django avec import automatique de modèles

La documentation de django_extensions précise comment démarrer.


Démarrer la console interactive Django

Installez d'abord les bibliothèques :

pip install django-extensions ipython

Ajoutez django_extensions à settings.INSTALLED_APPS{.python} puis lancez la commande de gestion shell_plus :

python manage.py shell_plus

Créer des enregistrements

Considérons à nouveau notre modèle Character :

from django.db import models

class Character(models.Model):
    name = models.CharField(max_length=32)
    race = models.CharField(max_length=32)
    level = models.IntegerField()
    hp = models.IntegerField(default=10)
    mp = models.IntegerField(default=0)

Il existe deux façons de créer un enregistrement :

# Création indirecte
cyril = Character(name="Cyril", race="Elf", level=1, hp=50, mp=10)
lorna = Character(name="Lorna", race="Elf", level=1, hp=50, mp=10)
cyril.save()
lorna.save()

Nous créons deux instances de Character et les enregistrons dans la base de données.


Nous pouvons également utiliser une méthode du modèle pour créer un enregistrement. Nous passons par un attribut du modèle nommé objects, qui est un gestionnaire permettant d'effectuer des requêtes sur la base de données.

# Création directe
cyril = Character.objects.create(name="Cyril", race="Elf", level=1, hp=50, mp=10)
lorna = Character.objects.create(name="Lorna", race="Elf", level=1, hp=50, mp=10)

Récupérer un enregistrement

Il arrive régulièrement de souhaiter récupérer un enregistrement de la base, et un seul. Par exemple, si une vue de votre projet permet d'afficher les informations d'une entité de votre base de données, vous devez retrouver l'entité, ou afficher une page 404 si elle n'existe pas.

Le gestionnaire objects du modèle possède une méthode get() qui permet de retrouver un enregistrement et un seul. Par exemple :

from django.http import Http404

def show_character(request, name: str):
    try:
        character = Character.objects.get(name=name)
        ...
    except Character.DoesNotExist:
        raise Http404("Character not found")

L'exemple ci-dessus est une vue qui affiche une page 404 si l'enregistrement n'a pas été trouvé.


Récupérer tous les enregistrements d'une table

L'usage le plus courant des bases de données est de requêter à la recherche de plusieurs enregistrements. Django propose un type d'objet representant une liste d'enregistrements, appelé QuerySet. Chaque élément d'un QuerySet est logiquement une instance du modèle.

Un QuerySet est obtenu lorsqu'on appelle certaines méthodes sur le gestionnaire objects du modèle. Un QuerySet possède également des méthodes communes au gestionnaire objects.

queryset = Character.objects.all()
counter = queryset.count()  # renvoie le nombre d'enregistrements

# Un QuerySet est un itérateur
for character in queryset:
    # Chaque élément est un objet de type Character
    print(character.name)

Filtrer les enregistrements

Pour filtrer vos enregistrements correspondant à des critères de recherche, les gestionnaires objects et les QuerySet possèdent des méthodes filter et exclude. Par exemple, si je souhaite récupérer les enregistrements dont la colonne race possède exactement le texte Elf :

queryset = Character.objects.filter(race="Elf")

Il suffit de passer des arguments possédant le nom de colonnes du modèle. Vous pouvez passer plusieurs arguments pour filtrer davantage sur plusieurs colonnes (avec un opérateur AND).


Exclure des enregistrements

C'est assez rare, mais si vous devez exclure des enregistrements, probablement d'un QuerySet, n'oubliez pas la méthode exclude. Si vous souhaitez par exemple récupérer tous les elfes dont le nom n'est pas "Lucas" :

queryset = Character.objects.filter(race="Elf").exclude(name="Lucas")

Filtrer en AND et OR

Pour filtrer simplement avec filter, vous pouvez passer des arguments nommés comme vos colonnes... mais cette technique génère uniquement une combinaison de filtres avec l'opérateur AND.

Pour choisir des combinaisons de filtres AND ou OR, vous devez plutôt passer aux méthodes filter ou exclude un argument positionnel composé d'objets de type Q :

from django.db.models import Q

level1_elves = Character.objects.filter(Q(race="Elf") & Q(level=1))
level1_or_11 = Character.objects.filter(Q(level=1) | Q(level=11))

Filtrer autrement qu'avec des valeurs exactes

Jusque là, notre seul outil pour filtrer est d'utiliser des arguments nommés tels les colonnes de nos modèles. Cependant, cette méthode ne nous permet de filtrer que par valeurs précises. Pourtant, il paraît facile d'imaginer que l'on voudrait filtrer, par exemple, sur des personnages dont le niveau excède une valeur, ou dont le nom commence par une lettre.

Django nous offre cette possibilité, en utilisant... encore des arguments nommés. Par exemple, si nous voulons obtenir tous les personnages dont le niveau est supérieur ou égal à 10, nous pouvons faire

queryset = Character.objects.filter(level__gte=10)

Le texte __ est utilisé par Django pour découper le nom de l'argument en plusieurs parties. La première partie, level, indique que nous travaillons sur le champ level du modèle Character. La deuxième partie, gte, indique à Django que nous voulons utiliser un opérateur de comparaison. Dans ce cas, le mot gte signifie greater than or equal to.


Les opérateurs proposés par Django permettent de modifier la comparaison ou de calculer des transformations sur un champ :

Opérateur Description
__gt plus grand que
__lt plus petit que
__gte égal ou supérieur à
__lte égal ou inférieur à
__range intervalle (tuple)
__in fait partie d'un ensemble

Ces opérateurs fonctionnent sur les champs de type int, float, date et datetime. Potentiellement, on peut aussi utiliser ces opérateurs sur les champs de type bool et str.


Pour les chaînes de caractères, Django offre des opérateurs supplémentaires :

Opérateur Description
__exact identique à aucun opérateur
__iexact rechercher en ignorant la casse des caractères
__startswith/istartswith commence par cette chaîne
__endswith/iendswith commence par cette chaîne
__contains/icontains contient cette chaîne

Voici un exemple d'usage de cette catégorie d'opérateurs :

# Récupérer les personnages ayant un nom contenant "Lucas" en ignornant la casse
queryset = Character.objects.filter(name__icontains="lucas")

Ce n'est pas fini, il existe aussi des opérateurs pour extraire des informations depuis des datetime et date :

Opérateur Description
__year extrait l'annee
__month extrait le mois
__day extrait le jour
__hour extrait l'heure
__minute extrait les minutes
__second extrait les secondes
__week extrait la semaine
__isoweek extrait la semaine ISO
__weekday extrait le jour de la semaine

Voici un autre exemple, qui est intéressant lorsque des opérateurs récupèrent une partie d'une information :

# Récupérer des rendez-vous de lundi et mardi
queryset = Appointment.objects.filter(when__weekday__in=(0, 1))

Vous pouvez effectivement extraire le jour de la semaine d'une date (sous la forme d'un int{.python}) et comparer cette valeur en y apposant un opérateur de comparaison.


Ordonner les enregistrements

Vous pouvez organiser les résultats de vos QuerySet avec la méthode order_by.

# Trier par nom croissant
by_name_asc = Character.objects.filter(race="Elf").order_by("name")
# Trier par nom, en ordre décroissant alphanumériquement (dépendant de la collation)
by_name_desc = Character.objects.filter(race="Elf").order_by("-name")
# Mélanger aléatoirement les résultats
random_order = Character.objects.filter(race="Elf").order_by("?")
# Trier par niveau croissant, puis par nom, en ordre décroissant
by_level_asc_and_name_desc = Character.objects.filter(race="Elf").order_by("level", "-name")

Créer et gérer des relations entre enregistrements

Les bases de données relationnelles ont la particularité de permettre la définition de relations entre tables. Django vous propose des champs de modèles permettant de gérer les relations classiques. Il en existe 3 types :

Type de relation Description
OneToOneField une relation 1 à 1
ForeignKey une relation 1 à N
ManyToManyField une relation plusieurs à plusieurs

Relations 1 à 1 (OneToOneField)

Une relation 1 à 1 vous permet de créer une relation bijective entre les éléments de deux tables. Par exemple, si vous utilisez un modèle que vous n'avez pas l'autorisation de modifier, mais que vous avez besoin d'attacher des données supplémentaires à celui-ci, vous pouvez créer un modèle avec une relation 1 à 1 vers celui-ci.

from django.db import models

class User(models.Model):
    """Utilisateur. Modèle en lecture seule."""
    username = models.CharField(max_length=30)

class Profile(models.Model):
    # Un utilisateur peut avoir un seul profil
    user = models.OneToOneField("application.User", on_delete=models.CASCADE)
    picture = models.ImageField(max_length=256, null=True, upload_to='pictures', verbose_name='picture')

Ici, une clé étrangère avec contrainte d'unicité sera créée par Django. Le premier argument de OneToOneField est une classe de modèle, ou un str contenant le nom du modèle (recommandé). Le deuxième argument est on_delete, qui permet de définir ce qui doit se passer lorsque l'utilisateur est supprimé. Dans notre cas, on veut supprimer le profil aussi.


Valeurs possibles de l'argument on_delete

Les champs ForeignKey et OneToOneField permettent de choisir le comportement à adopter lorsque les enregistrements ciblés par une relation sont supprimés.

Valeur Description
models.DO_NOTHING ne rien faire. Impossible la plupart du temps
models.SET_NULL mettre null. Nécessite null=True
models.SET_DEFAULT mettre une valeur default
models.CASCADE supprimer en cascade
models.PROTECT interdire la suppression

Utilisation des relations 1 à 1

Une fois que vous avez un modèle possédant une clé étrangère en 1-1, vous avez accès la clé sur les deux modèles; dans notre cas, vous aurez accès à :

Modèle Champ du modèle
Profile user
User profile (par défaut)
# Créer un utilisateur
user = User.objects.create(username="bob")
# Créer son profil
profile = Profile.objects.create(user=user)
# Afficher le profil de bob
print(user.profile)
# Afficher l'utilisateur du profil de bob
print(profile.user)

Attention toutefois, si l'utilisateur n'a pas de profil associé, user.profile générera une exception de type RelatedObjectDoesNotExist. Il faudra intercepter ce cas de figure si nécessaire.

try:
    print(user.profile)
except Profile.DoesNotExist:
    print("No profile for this user")

Relations 1 à N (ForeignKey)

Une relation 1 à N vous permet de créer une relation libre entre les éléments de deux tables. Par exemple, si on souhaite associer une classe à nos personnages (la classe possédant des propriétés), on peut imaginer déclarer un modèle de classe :

from django.db import models

class CharacterClass(models.Model):
    name = models.CharField(max_length=30)
    description = models.TextField()

class Character(models.Model):
    name = models.CharField(max_length=32)
    race = models.CharField(max_length=32)
    level = models.IntegerField()
    character_class = models.ForeignKey("CharacterClass", on_delete=models.PROTECT, related_name="characters")

L'argument related_name permet de choisir le nom de la relation inverse, par exemple si vous avez un modèle de personnage et un modèle de classe, vous pouvez choisir de nommer la relation inverse de character_class en characters. Vous pouvez effectuer des requêtes avec les relations :

# Récupérer tous les personnages de la classe Mage
characters = Character.objects.filter(character_class__name="Mage")

# Récupérer toutes les classes ayant des personnages de race Elf
# Attention, il peut y avoir des doublons !
classes = CharacterClass.objects.filter(characters__race="Elf")

Si vous ne précisez pas de related_name, Django choisira un nom automatiquement. Par exemple, si vous avez un modèle de personnage Character et un modèle de classe CharacterClass, le nom de la relation inverse de character_class sera character_set.


Gestion des doublons

Dès lors que vous effectuez des requêtes impliquant des relations (jointures en SQL), il peut y avoir des doublons. Par exemple, si on souhaite récupérer les classes des personnages de race Elf, votre QuerySet pourrait contenir des éléments en double; la raison est très simple :

La table de résultats de la jointure générée par la base de données SQL ressemblera au tableau suivant (les colonnes de gauche concernent CharacterClass, celles de droite Character) :

id name description id name race level character_class_id
1 Mage Mage 1 Lorna Elf 1 1
1 Mage Mage 2 Cyril Elf 1 1
2 Cleric Cleric 3 Aaron Elf 1 2
2 Cleric Cleric 4 Diane Elf 1 2

Vous récupérez donc deux fois la classe Mage, et deux fois la classe Cleric. Pour dédupliquer les résultats, vous pouvez utiliser la méthode distinct sur votre QuerySet :

# Récupérer toutes les classes ayant des personnages de race Elf
classes = CharacterClass.objects.filter(characters__race="Elf").distinct()

Relations N à N (ManyToManyField)

Les bases de donné

Les opérateurs présentés ci-dessus sont disponibles dans la documentation officielle de Django à cette page :

Opérateurs de recherche


Filtrage du type critère1 OU critère2

Par défaut, avec filter, passer plusieurs arguments de critères permet de retourner un QuerySet qui répond à tous les critères (critère1 ET critère2 etc…). Pour combiner des critères avec des opérateurs ET/OU, il faut utiliser des objets Q :

from django.models import Q

MyModel.objects.filter(Q(name="Connelly") | Q(name="Hugo"))

Récupérer un seul objet plutôt qu'un QuerySet

Des fois, plutôt que de récupérer un QuerySet, on voudra faire une requête pour savoir si un et un seul objet existe dans notre base (ex. trouver un objet avec un id particulier pour une page de consultation)


Le manager Model.objects propose une méthode get() qui permet de récupérer directement un objet plutôt qu'un QuerySet, mais qui renvoie une erreur s'il n'existe pas un et un seul objet dans la table répondant à nos critères. Elle s'utilise de la même façon que la méthode filter().

# peut lever `ObjectDoesNotExist` ou `MultipleObjectsReturned`
item = MyModel.objects.get(id=19)  

Définition des relations entre tables

Les bases de données relationnelles ont, comme leur nom l'indique, des champs dont le rôle est de définir et contraindre des relations entre tables. Django prend ce type de champs en charge via des classes de champs spécifiques :

Champ de Modèle Description
ForeignKey relation plusieurs à un
OneToOneField relation un à un
ManyToManyField relation plusieurs à plusieurs

Exemple de modèle avec une relation

from django.db import models

class Category(models.Model):
    """Catégorie d'article."""
    name = models.CharField(max_length=32)

class Article(models.Model):
    """Article de blog."""
    title = models.CharField(max_length=32)
    category = models.ForeignKey("application.Category", on_delete=models.CASCADE)

Lorsqu'on ajoute un champ de relation, Django nous oblige à indiquer quelle stratégie adopter lorsqu'un enregistrement ciblé par une relation est supprimé. Ce comportement doit être indiqué via un argument nommé on_delete dans la définition du champ :

from django.db import models

class MyModel(models.Model):
    relation = models.ForeignKey("application.Model", on_delete=models.CASCADE)

Lorsqu'un enregistrement ciblé par une relation est supprimé, les stratégies les plus fréquemment utilisées sont les suivantes :

  • models.CASCADE : supprimer également l'enregistrement qui contient la relation
  • models.SET_NULL : si autorisé, retirer la relation de l'objet source
  • models.SET_DEFAULT : si default est précisé, remplacer la relation par la valeur par défaut
  • models.PROTECT : empêcher la suppression de la cible et lever une exception

Lorsque notre définition de modèles est prête, nous pouvons créer notre base et/ou nos tables en utilisant des commandes de gestion de notre projet Django :

./manage.py makemigrations <appname>  # créer des migrations
./manage.py migrate  # met à jour la base en suivant les migrations créées

Héritage, proxy et modèles abstraits

Avec Django, il est possible d'utiliser une classe de modèle héritant d'une autre.

class MyModel2(MyModel):
    ...

Héritage multi-tables

Lorsque l'on hérite d'une autre classe de modèle de cette façon, Django fait une chose très particulière : les objets de MyModel2 possèdent un lien vers un objet de MyModel1. Le système est compliqué à utiliser mais est bien documenté sur le site officiel .


Plus intéressant à mon sens, Django propose ce que l'on appelle des modèles abstraits.
Il s'agit simplement de classes de modèles contenant des options Meta où l'attribut abstract est à True. Qu'est-ce que ça veut dire en pratique ?


Un modèle possédant une option Meta.abstract = True n'est en fait pas créé dans la base de données, mais on peut en hériter et profiter de ses champs déjà créés.

:::notes En montrer un exemple avec un modèle abstrait possédant uuid, nom et description. :::


Moins intéressant à un niveau débutant que le modèle abstrait, le modèle dit "proxy" permet d'accéder aux données d'un autre modèle, mais permet d'ajouter ses propres méthodes sans toucher au modèle original. (typiquement ce qui se produit lorsqu'on souhaite augmenter des modèles d'une application que l'on a pas écrite)


Pour écrire un modèle proxy, il suffit de définir son option Meta proxy = True :

from django.db import models


class MyModel(models.Model):
    field1 = models.Field()  # incorrect, juste pour l'exemple

    class Meta:
        proxy = True


Requêter avec des ForeignKey et ManyToManyField etc.

Lorsque vos modèles possèdent des champs de relations, Django vous fournit des moyens efficaces d'effectuer des requêtes parcourant lesdites relations (comme vous le feriez en SQL avec des JOIN) .


Par exemple, si vous possédez un modèle A possédant une ForeignKey nommée b vers le modèle B , vous pouvez filtrer vos résultats de A de plusieurs façons :

b_instance = "Imaginez une instance de B"
qs = A.objects.filter(b=b_instance)
qs = A.objects.filter(b__field="valeur")
qs = A.objects.filter(b__field2__gt=0)

Et encore mieux, avec l'existence d'une clé étrangère depuis A vers B, Django vous propose implicitement de faire des requêtes sur B en filtrant sur des relations avec A, via le nom du modèle en minuscules :

qs = B.objects.filter(a="Imaginez une instance de A")
qs = B.objects.filter(a__field=valeur)
qs = B.objects.filter(a__autre_relation__field=valeur)

Lorsque plusieurs relations miroir existent vers le même modèle, Django va vous afficher une erreur, car le nom généré par Django des deux relations miroir provoque un conflit. Dans ce cas-là, il faut explicitement configurer le nom d'attribut généré automatiquement pour la relation miroir :

from django.db import models

# L'argument `related_name` permet de définir le nom du champ miroir dans la table cible
field = models.ForeignKey(
    "app.Model",
    on_delete=models.CASCADE,
    related_name="nouveau_nom")

Une fois ce related_name défini, vos requêtes utilisant la relation miroir deviennent :

qs = B.objects.filter( < nouveau_nom > __field = valeur)

Les règles énoncées précédemment s'appliquent de la même façon aux champs OneToOneField et ManyToManyField.


Enregistrer et modifier des données dans la base

Pour créer de nouveaux objets dans notre table, c'est assez simple :

item = MyModel(field1="", field2=0)
item.save()  # crée le nouvel objet et lui attribue un nouvel `id`

Pour modifier et mettre à jour un objet, c'est simple. Récupérez un objet qui a déjà un id (avec la méthode get() par exemple), modifiez ses champs et sauvez-le :

item = MyModel.objects.get(id=19)
item.field3 = "nouvelle valeur"
item.save()

On peut aussi mettre à jour tout un QuerySet d'un coup :

# Ici on va changer la valeur du champ `field1` dans toute la table
MyModel.objects.all().update(field1=valeur)

Aparté sur les ManyToManyField

Les champs ManyToManyField sont gérés de façon assez spécifique, car Django génère automatiquement des tables intermédiaires dans la base de données. La documentation officielle fournit des exemples de l'utilisation de ce champ.


Exécution de requêtes en SQL brut (déconseillé)

Django autorise l'écriture de requêtes en utilisant du SQL, mais comme la documentation l'indique, dans 95% des cas vous pourriez en réalité utiliser l'ORM pour faire ce que vous souhaitez.

:::notes Aller sur la page de la doc officielle :::


Système de migration de schéma

Concept de migrations


À chaque fois que vous modifiez vos modèles, il faut créer un delta, un fichier de migration qui contient la différence entre l'état précédent de vos modèles et le nouvel état :

./manage.py makemigrations <nom app>

Si cela est nécessaire, cela crée un nouveau fichier de migration dans le répertoire migrations de notre application.
Vous pourrez ensuite, sur les serveurs où votre application a déjà été utilisée, parvenir facilement à une base de données cohérente avec la nouvelle définition de vos modèles en utilisant :

./manage.py migrate

Modèles abstraits

Les modèles abstraits, qu'est-ce que c'est ? Ce sont des classes de modèles qui peuvent vous servir de base pour composer vos modèles.

Par exemple, si vous souhaitez avoir quatre modèles qui ont tous en commun un champ description , uuid et une méthode get_short_description(), vous pourriez définir quatre fois ces champs et la même méthode... ou alors


from django.db import models
from uuid import uuid4


class Model1(models.Model):
    description = models.TextField(verbose_name="Description")
    # Notez que la valeur par défaut est une référence de fonction
    uuid = models.UUIDField(default=uuid4, verbose_name="UUID")

    def get_short_description(self):
        return self.description[:100]

Sans compter les champs spécifiques à chaque modèle, il faudrait réécrire la même chose 4 fois !


from django.db import models
from uuid import uuid4


class BaseModel(models.Model):
    description = models.TextField(verbose_name="Description")
    uuid = models.UUIDField(default=uuid4, verbose_name="UUID")

    class Meta:
        abstract = True  # Ici, le modèle n'est pas pris en compte pour les migrations

    def get_short_description(self):
        return self.description[:100]


class Model1(BaseModel):  # On hérite de BaseModel et donc de Model
    pass

Avec une classe abstraite, nous avons écrit les infos communes une seule fois, et hérité 4 fois.


Récapitulatif minimal des modèles

  1. Créer une application pour contenir un ou plusieurs modèles
  2. Créer un ou plusieurs modèles héritant de django.models.Model
  3. S'assurer que le modèle contient bien toutes les informations nécessaires
  4. Créer un nouveau fichier de migration (cliché des modèles à un instant t)
  5. Appliquer la définition des nouveaux fichiers de migration à la base