Maîtriser les modèles Django : champs, relations et performances

Champs intégrés et paramètres courants

Types de champs fréquemment utilisés

models.AutoField(primary_key=True)
// Clé primaire entière auto-incrémentée

models.IntegerField()
// Entier signé (-2147483648 à 2147483647)

models.CharField(max_length=255)
// Chaîne de caractères avec longueur maximale obligatoire

models.DateTimeField(auto_now_add=True)
// Stocke l’heure de création (une seule fois)

models.DateTimeField(auto_now=True)
// Met à jour automatiquement à chaque sauvegarde

models.DateField()
// Format YYYY-MM-DD

models.TimeField()
// Format HH:MM:SS[.uuuuuu]

models.BooleanField()
// En base : 0 ou 1 ; en Python : True/False

models.TextField()
// Pour des contenus longs (articles, descriptions, etc.)

models.FileField(upload_to='uploads/files/')
// Stocke le chemin du fichier ; le fichier est physiquement déposé dans le dossier spécifié

models.ImageField(upload_to='uploads/images/', height_field='height', width_field='width')
// Sous-classe de FileField avec validation d’image et extraction automatique des dimensions

models.DecimalField(max_digits=10, decimal_places=2)
// Nombre décimal précis (ex. : montants financiers)

models.EmailField()
// Valide le format email ; stocké comme VARCHAR en base

Correspondance ORM ↔ MySQL

ORM MySQL
AutoField INT AUTO_INCREMENT PRIMARY KEY
CharField(max_length=100) VARCHAR(100)
IntegerField INT
DecimalField(max_digits=8, decimal_places=2) DECIMAL(8,2)
DateField DATE
DateTimeField DATETIME
BooleanField TINYINT(1)
TextField TEXT
FileField VARCHAR (stocke un chemin)

Paramètres universels des champs

null=True          // Autorise NULL en base (à distinguer de blank=True pour les formulaires)
blank=True         // Autorise la valeur vide dans les formulaires
default=0          // Valeur par défaut lors de la création (peut être une fonction, ex: timezone.now)
unique=True        // Contrainte d’unicité au niveau base
db_index=True      // Crée un index B-tree sur ce champ
primary_key=True   // Définit la clé primaire (remplace AutoField si utilisé avec IntegerField)
verbose_name="Nom affiché"  // Libellé lisible dans l’admin et les formulaires
help_text="Indication supplémentaire"  // Texte d’aide dans les formulaires
db_column="col_name"       // Nom personnalisé de la colonne en base

Extensions avancées des modèles

Création de champs personnalisés

Pour imposer un type SQL spécifique non fourni nativement (ex. : CHAR(N) fixe), on étend models.Field :

from django.db import models

class FixedLengthCharField(models.Field):
    def __init__(self, length, *args, **kwargs):
        self.length = length
        super().__init__(*args, **kwargs)

    def db_type(self, connection):
        return f'char({self.length})'

class Product(models.Model):
    sku = models.CharField(max_length=12)  # standard
    code = FixedLengthCharField(length=8)   # personnalisé → CHAR(8)

Gestion des valeurs énumérées avec choices

Plutôt que de stocker des chaînes répétitives (ex. : "Homme", "Femme"), on utilise des identifiants compacts :

class UserProfile(models.Model):
    GENDER_CHOICES = [
        (0, 'Non renseigné'),
        (1, 'Masculin'),
        (2, 'Féminin'),
        (3, 'Autre'),
    ]
    
    gender = models.SmallIntegerField(
        choices=GENDER_CHOICES,
        default=0
    )
    
    # Usage dans le code :
    # instance.gender → retourne 1 (valeur brute)
    # instance.get_gender_display() → retourne 'Masculin'

Les choix peuvent aussi utiliser des chaînes comme clés :

STATUS_CHOICES = [
    ('draft', 'Brouillon'),
    ('published', 'Publié'),
    ('archived', 'Archivé'),
]
status = models.CharField(max_length=12, choices=STATUS_CHOICES)

Modélisation des relations many-to-many

Approche 1 : Relation implicite (ManyToManyField)

class Article(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField('Tag')

class Tag(models.Model):
    name = models.CharField(max_length=50)

✅ Avantages : API riche (add(), remove(), set(), clear()), accès bidirectionnel.
❌ Inconvénient : impossible d’ajouter des champs métiers (ex. : date de liaison, rôle, poids) à la table intermédiaire.

Approche 2 : Table intermédiaire explicite

class Article(models.Model):
    title = models.CharField(max_length=200)

class Tag(models.Model):
    name = models.CharField(max_length=50)

class ArticleTag(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    weight = models.PositiveSmallIntegerField(default=1)
    created_at = models.DateTimeField(auto_now_add=True)

✅ Avantages : totale flexibilité (champs supplémentaires, contraintes, index).
❌ Inconvénient : aucune méthode ORM native — toutes les opérations se font via ArticleTag.objects.create(...).

Approche 3 : Hybridation (through)

class Article(models.Model):
    title = models.CharField(max_length=200)
    tags = models.ManyToManyField(
        'Tag',
        through='ArticleTag',
        through_fields=('article', 'tag')
    )

class Tag(models.Model):
    name = models.CharField(max_length=50)

class ArticleTag(models.Model):
    article = models.ForeignKey(Article, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    weight = models.PositiveSmallIntegerField(default=1)

✅ Avantages : accès bidirectionnel (article.tags.all(), tag.article_set.all()) + données enrichies.
❌ Inconvénient : les méthodes add()/remove() ne sont pas disponibles — il faut créer manuellement les instances ArticleTag.

Optimisation des écritures massives

Insertion séquentielle (inefficace)

# À éviter pour >100 objets
for i in range(10000):
    Book.objects.create(title=f'Livre {i}')
# → 10 000 requêtes INSERT distinctes

Insertion groupée avec bulk_create

books = [Book(title=f'Livre {i}') for i in range(10000)]
Book.objects.bulk_create(books, batch_size=1000)
# → 10 requêtes INSERT (10 × 1000 lignes), bien plus rapide

Remarques :
- batch_size limite la taille de chaque insrtuction SQL (évite les timeouts ou dépassements mémoire).
- Les objets retournés n’ont pas encore d’id si la base ne supporte pas RETURNING (ex. : MySQL avant 8.0.21).
- Aucun signal post_save n’est déclenché — à gérer manuellement si nécessaire.

Performance des requêtes paginées

Une pagination naïve avec LIMIT offset, count devient coûteuse sur de grands jeux :

-- Première page : rapide (lit ~50 lignes)
SELECT * FROM book LIMIT 0, 50;

-- Dernière page : lent (lit 100 000 lignes, en ignore 99 950)
SELECT * FROM book LIMIT 99950, 50;

Solutions recommandées :
• Pagination basée sur la dernière valeur lue (« keyset pagination ») : WHERE id > 99950 ORDER BY id LIMIT 50.
• Utilisation de select_related() / prefetch_related() pour réduire les requêtes N+1.
• Index appropriés sur les champs utilisés dans ORDER BY et WHERE.

Étiquettes: Django ORM MySQL many-to-many bulk-create

Publié le 30 septembre à 01h59