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

![Concept de migrations](assets/images/orm-model-migration-concept.png)

----

À 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
