1109 lines
39 KiB
Markdown
1109 lines
39 KiB
Markdown
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](https://docs.djangoproject.com/en/dev/ref/settings/#databases),
|
||
pour pouvoir gérer en même temps plus bases de données, de types différents qui plus est !
|
||
|
||
```{.python .numberLines}
|
||
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` :
|
||
|
||
```python {.numberLines}
|
||
from django.db import models
|
||
|
||
|
||
class MyModel(models.Model):
|
||
champ1 = models.CharField(max_length=32)
|
||
```
|
||
|
||
[Introduction sur les modèles](https://docs.djangoproject.com/fr/5.1/topics/db/models/)
|
||
|
||
----
|
||
|
||
### 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 :
|
||
|
||
```python {.numberLines}
|
||
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](https://docs.djangoproject.com/fr/5.1/ref/models/fields/#field-types)
|
||
|
||
----
|
||
|
||
|
||
### 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](https://docs.djangoproject.com/en/dev/ref/models/fields/) 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
|
||
|
||
```python {.numberLines}
|
||
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](https://docs.djangoproject.com/en/dev/ref/models/options/) sous la forme d'attributs dans cette classe.
|
||
|
||
----
|
||
|
||
```python
|
||
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](https://docs.djangoproject.com/en/dev/ref/models/options/).
|
||
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](https://docs.djangoproject.com/en/dev/topics/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.
|
||
|
||
```python {.numberLines}
|
||
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 :
|
||
|
||
```python {.numberLines}
|
||
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`](https://django-extensions.readthedocs.io/en/latest/installation_instructions.html)
|
||
précise comment démarrer.
|
||
|
||
----
|
||
|
||
### Démarrer la console interactive Django
|
||
|
||
Installez d'abord les bibliothèques :
|
||
|
||
```bash {.numberLines}
|
||
pip install django-extensions ipython
|
||
```
|
||
|
||
Ajoutez `django_extensions` à `settings.INSTALLED_APPS`{.python} puis lancez la commande de gestion `shell_plus` :
|
||
|
||
```bash {.numberLines}
|
||
python manage.py shell_plus
|
||
```
|
||
|
||
----
|
||
|
||
### Créer des enregistrements
|
||
|
||
Considérons à nouveau notre modèle `Character` :
|
||
|
||
```python {.numberLines}
|
||
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 :
|
||
|
||
```python {.numberLines}
|
||
# 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.
|
||
|
||
```python {.numberLines}
|
||
# 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 :
|
||
|
||
```python {.numberLines}
|
||
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`.
|
||
|
||
```python {.numberLines}
|
||
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` :
|
||
|
||
```python {.numberLines}
|
||
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"` :
|
||
|
||
```python {.numberLines}
|
||
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` :
|
||
|
||
```python {.numberLines}
|
||
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
|
||
|
||
```python {.numberLines}
|
||
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 :
|
||
|
||
```python {.numberLines}
|
||
# 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 :
|
||
|
||
```python {.numberLines}
|
||
# 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`.
|
||
|
||
```python {.numberLines}
|
||
# 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.
|
||
|
||
```python {.numberLines}
|
||
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) |
|
||
|
||
|
||
|
||
```python {.numberLines}
|
||
# 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.
|
||
|
||
```python {.numberLines}
|
||
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 :
|
||
|
||
```python {.numberLines}
|
||
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 :
|
||
|
||
```python {.numberLines}
|
||
# 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` :
|
||
|
||
```python {.numberLines}
|
||
# 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](https://docs.djangoproject.com/en/dev/ref/models/querysets/#field-lookups)
|
||
|
||
----
|
||
|
||
### 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](https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects) :
|
||
|
||
```python
|
||
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()`.
|
||
|
||
```python
|
||
# 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
|
||
|
||
```python {.numberLines}
|
||
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 :
|
||
|
||
```python
|
||
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 :
|
||
|
||
```shell_script
|
||
./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.
|
||
|
||
```python
|
||
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](https://docs.djangoproject.com/en/dev/topics/db/models/#multi-table-inheritance)
|
||
.
|
||
|
||
----
|
||
|
||
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` :
|
||
|
||
```python
|
||
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 :
|
||
|
||
```python
|
||
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 :
|
||
|
||
```python
|
||
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 :
|
||
|
||
```python
|
||
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 :
|
||
|
||
```python
|
||
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 :
|
||
|
||
```python
|
||
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 :
|
||
|
||
```python
|
||
item = MyModel.objects.get(id=19)
|
||
item.field3 = "nouvelle valeur"
|
||
item.save()
|
||
```
|
||
|
||
----
|
||
|
||
On peut aussi mettre à jour tout un `QuerySet` d'un coup :
|
||
|
||
```python
|
||
# 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](https://docs.djangoproject.com/en/dev/topics/db/examples/many_to_many/)
|
||
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 :
|
||
|
||
```bash
|
||
./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 :
|
||
|
||
```bash
|
||
./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
|
||
|
||
----
|
||
|
||
```python
|
||
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 !
|
||
|
||
----
|
||
|
||
```python
|
||
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
|