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 :
- Configurer les bases de données dans
settings.DATABASES
{.python} - Ajouter une application dans
settings.INSTALLED_APPS
{.python} - 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)
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
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 :
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 relationmodels.SET_NULL
: si autorisé, retirer la relation de l'objet sourcemodels.SET_DEFAULT
: sidefault
est précisé, remplacer la relation par la valeur par défautmodels.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
À 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
- Créer une application pour contenir un ou plusieurs modèles
- Créer un ou plusieurs modèles héritant de
django.models.Model
- S'assurer que le modèle contient bien toutes les informations nécessaires
- Créer un nouveau fichier de migration (cliché des modèles à un instant t)
- Appliquer la définition des nouveaux fichiers de migration à la base