Vue d'ensemble

Django 4.1, publié le 3 août 2022, marque une étape importante dans la transition vers un Django asynchrone. L'ORM gagne une interface async complète avec des méthodes comme acreate(), aget() et afilter(). Les vues basées sur les classes deviennent compatibles async, et la validation des modèles s'enrichit de nouvelles possibilités.

Cette version prépare le terrain pour un écosystème Django pleinement asynchrone, en rendant les opérations de base de données les plus courantes accessibles depuis du code async/await.

Fonctionnalités principales

Interface ORM asynchrone

Le QuerySet de Django propose désormais des variantes asynchrones des méthodes les plus utilisées. Chaque méthode synchrone a son équivalent préfixé par a : aget(), acreate(), acount(), aexists(), etc. On peut également itérer de manière asynchrone sur un QuerySet avec async for.

python
from django.http import JsonResponse

# Requêtes asynchrones dans une vue async
async def api_articles_recents(request):
    """Renvoie les 10 derniers articles publiés au format JSON."""
    articles = []
    async for article in (
        Article.objects
        .filter(publie=True)
        .order_by('-date_publication')[:10]
    ):
        articles.append({
            'titre': article.titre,
            'resume': article.resume,
            'date': article.date_publication.isoformat(),
        })
    return JsonResponse({'articles': articles})

# Création et récupération asynchrones
async def inscrire_participant(request):
    """Inscrit un participant à une conférence."""
    conference = await Conference.objects.aget(pk=request.POST['conf_id'])
    nb_inscrits = await conference.participants.acount()

    if nb_inscrits >= conference.capacite:
        return JsonResponse({'erreur': 'Complet'}, status=409)

    participant, cree = await Participant.objects.aget_or_create(
        email=request.POST['email'],
        defaults={'nom': request.POST['nom']},
    )
    await conference.participants.aadd(participant)
    return JsonResponse({'inscrit': cree, 'total': nb_inscrits + 1})

Vues basées sur les classes async

Les vues génériques basées sur les classes (CBV) supportent désormais les handlers asynchrones. Il suffit de définir les méthodes get(), post(), etc. comme des coroutines async def. Django détecte automatiquement si la vue est synchrone ou asynchrone.

python
from django.views import View
from django.http import JsonResponse
import httpx

class MeteoView(View):
    """Vue asynchrone qui interroge une API externe."""

    async def get(self, request, ville):
        async with httpx.AsyncClient() as client:
            reponse = await client.get(
                f'https://api.meteo.example/v1/{ville}'
            )
        donnees = reponse.json()

        # Enregistrer la consultation en base (async)
        await Consultation.objects.acreate(
            ville=ville,
            utilisateur=request.user if request.user.is_authenticated else None,
        )

        return JsonResponse({
            'ville': ville,
            'temperature': donnees['temp'],
            'conditions': donnees['description'],
        })

# urls.py
# path('meteo/<str:ville>/', MeteoView.as_view(), name='meteo')

Améliorations de la validation des modèles

Django 4.1 améliore la validation des modèles en permettant d'ajouter des messages d'erreur personnalisés plus facilement dans la méthode clean(). Le système de contraintes gagne également l'attribut violation_error_message pour personnaliser le message d'erreur lorsqu'une contrainte est violée au niveau de la base de données.

python
from django.db import models
from django.db.models import Q, F, CheckConstraint
from django.core.exceptions import ValidationError

class Facture(models.Model):
    reference = models.CharField(max_length=20, unique=True)
    montant_ht = models.DecimalField(max_digits=10, decimal_places=2)
    taux_tva = models.DecimalField(max_digits=4, decimal_places=2)
    montant_ttc = models.DecimalField(max_digits=10, decimal_places=2)
    date_emission = models.DateField()
    date_echeance = models.DateField()

    class Meta:
        constraints = [
            CheckConstraint(
                check=Q(montant_ht__gt=0),
                name='facture_montant_positif',
                violation_error_message=(
                    'Le montant HT doit être strictement positif.'
                ),
            ),
            CheckConstraint(
                check=Q(date_echeance__gte=F('date_emission')),
                name='facture_echeance_apres_emission',
                violation_error_message=(
                    "La date d'échéance ne peut précéder "
                    "la date d'émission."
                ),
            ),
        ]

    def clean(self):
        """Vérifie la cohérence entre montant HT, TVA et TTC."""
        calcul_ttc = self.montant_ht * (1 + self.taux_tva / 100)
        if abs(self.montant_ttc - calcul_ttc) > 0.01:
            raise ValidationError({
                'montant_ttc': (
                    f'Le montant TTC ({self.montant_ttc}) ne correspond '
                    f'pas au calcul attendu ({calcul_ttc:.2f}).'
                )
            })

Sources