Accueil / Blog / Métier / 2016 / Django Rest Framework : fonctionnement des routeurs (partie 3)

Django Rest Framework : fonctionnement des routeurs (partie 3)

Par Yannick Chabbert — publié 07/01/2016, édité le 24/02/2016
Chose promise, chose due : après avoir mis en avant les bienfaits des Viewset, on va plonger dans le côté obscur de la force de DRF : les routeurs.
Django Rest Framework : fonctionnement des routeurs (partie 3)

Dans la continuité du précédent article sur les Viewset, on va maintenant analyser le fonctionnement des routeurs. Les routeurs ont la particularité de générer automatiquement les motifs d'urls des Viewset. Pour y parvenir, on enregistre d'abord des Viewset avec leurs préfixes de chemins. Puis pour chacunes des actions des Viewset (méthodes d'instance), le routeur déduira le motif d'url associé grâce à des routes. Ces routes sont des namedtuples qui définissent les motifs d'urls, leurs noms, et parfois même un mapping entre des actions standard et des méthodes HTTP.

Exemple d'utilisation d'un routeur

Avant de s'aventurer plus loin, on va se rafraîchir un peu la mémoire avec un cas concret. Admettons que l'on ai le Viewset suivant :

from django.db import models

from rest_framework import serializers
from rest_framework.mixins import (
    ListModelMixin, RetrieveModelMixin, UpdateModelMixin,
)
from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import GenericViewSet


class Book(models.Model):
    pass


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book


class BookViewset(ListModelMixin, RetrieveModelMixin, UpdateModelMixin,
                  GenericViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

On va d'abord créer une instance de classe de type routeur :

router = DefaultRouter()

Jusque là, rien d'incroyable. À cette instance, on enregistre le Viewset avec le préfix de chemin books :

router.register(r'books', BookViewset)

Pour ensuite générer une liste de motif d'urls en appellant la propriété urls de l'instance :

urlpatterns = router.urls

Voilà, la variable urlpatterns contient les motifs d'urls :

[
    <RegexURLPattern api-root ^$>,
    <RegexURLPattern api-root ^\.(?P<format>[a-z0-9]+)/?$>,
    <RegexURLPattern book-list ^books/$>,
    <RegexURLPattern book-list ^books\.(?P<format>[a-z0-9]+)/?$>,
    <RegexURLPattern book-detail ^books/(?P<pk>[^/.]+)/$>,
    <RegexURLPattern book-detail ^books/(?P<pk>[^/.]+)\.(?P<format>[a-z0-9]+)/?$>
]

Les mêmes que ceux qu'on aurait fait à la mano, mais avec les bonnes pratiques ! Faut bien reconnaitre que c'est cadeau.

Bon, l'introduction est terminée, on peut désormais entrer dans le vif du sujet.

Comment ça marche

Parce que vous savez que rien n'est magique en informatique, on va tenter de percer les mystères des routeurs. Pour cela, reprenons l'exemple précédent. Quand on a généré les motifs d'urls en appellant la propriété urls du routeur, c'est en réalité la méthode get_urls qui a été appellée une première fois pour mettre le résultat en cache. Donc si vous voulez l'appeller 40 fois pour le plaisir, pas de soucis vous pouvez. Dans cette méthode, pour chacun des Viewset enregistrés, le routeur va :

  • détecter les routes avec la méthode get_routes. Comme on reviendra en profondeur sur ce point plus tard, on ne va pas rentrer dans les détails. Retenez seulement que le routeur s'appuie sur l'attribut routes de la classe SimpleRouter. Cet attribut est une liste de namedtuple qui contiennent des urls, leurs noms et parfois un mapping entre le nom de méthodes de vues et des méthodes HTTP.
  • résoudre ces routes pour transformer les jetons comme les identifiants (lookup) et ainsi obtenir le pattern d'url final (regex).
  • créer une instance de motif d'url (RegexURLPattern) pour chacune de ces routes avec la regex, le nom et la fonction génératrice de vues du Viewset. Cette fonction est obtenue suite à l'appel de la méthode as_view() d'une nouvelle instance du Viewset. Cette méthode reçoit la liste des actions détectées. C'est ainsi que la fonction de vue connaît le couple méthodes d'instances / méthodes HTTP.

Concrètement, à chaque appel HTTP d'un de ces points d'entrée, le dispatcher de Django appelle la fonction génératrice pour le chemin demandé. Rien de nouveau, c'est le comportement classique de Django. Mais cette fonction génératrice va créer à la volée les méthodes d'instances de vues avec les noms des méthodes HTTP supportées. Puis elle finit par appeler la méthode dispatch() qui exécute la méthode d'instance (action).

Voilà vous savez toute la vérité...vous n'êtes pas trop triste ?

Les types de routes

Vous l'avez peut-être remarqué dans l'attribut routes. Il existe en effet par défaut deux (types de) routes :

  • detail: les urls contiennent l'identifiant de la ressource (lookup)
  • list: les urls ne contiennent pas d'identifiant de la ressource

Typiquement, si le préfix est books, vous aurez :

  • detail: /books/<pk>/
  • list: /books/

C'est ensuite la méthode HTTP qui déterminera l'action à exécuter pour un même chemin.

Ces deux types de routes couvrent la plupart des besoins et leurs usages sont assez ancrés dans DRF (décorateurs, liens d'API racine, etc). Néanmoins, on verra dans un prochain article que vous pouvez surcharger cette partie.

Les classes de routeurs

Il existe différentes classes de routeurs. La classe de base est SimpleRouter et la plus fréquemment (aveuglément) utilisée est la classe DefaultRouter, surcharge à la précédente. Elle apporte en plus :

  • les formats de suffixes
  • une vue racine auto-générée par le routeur qui retourne une liste d'url de tous les Viewset. Chaque url est la route list d'un Viewset. C'est principalement (uniquement) utile avec la browsable API. Le but étant de représenter l'arborescence de l'API.

Les puristes vous diront qu'il faut utiliser la classe SimpleRouter si vous n'avez pas besoin de tout ça. Les pragmatiques vous diront d'utiliser la classe DefaultRouter juste au cas où. À vous de voir.

Les routes standard

Pour répondre aux besoins classiques des API REST, DRF propose des routes standard. Ces routes définissent des motifs d'url et des méthodes HTTP pour des actions prédéfinies. Autrement dit, ces actions sont des méthodes d'instance dont le nom est réservé. Les routes standard sont déclarées en tant que namedtuples Route de l'attribut routes de la classe SimpleRouter :

# List route.
Route(
    url=r'^{prefix}{trailing_slash}$',
    mapping={
        'get': 'list',
        'post': 'create'
    },
    name='{basename}-list',
    initkwargs={'suffix': 'List'}
)
# Detail route.
Route(
    url=r'^{prefix}/{lookup}{trailing_slash}$',
    mapping={
        'get': 'retrieve',
        'put': 'update',
        'patch': 'partial_update',
        'delete': 'destroy'
    },
    name='{basename}-detail',
    initkwargs={'suffix': 'Instance'}
)

En regardant l'attribut mapping de ces namedtuples Route, on distingue les actions standard et leurs méthodes HTTP suivantes :

Actions Méthode HTTP
list GET
create POST
retrieve GET
update PUT
partial_update PATCH
destory DELETE

On remarque aussi la présence de deux routes. Ces deux routes sont en fait les deux types de routes respectives list et detail. Ci-dessous les actions standard regroupées par types de routes :

List Detail
list retrieve
create update
  partial_update
  destroy

Eh oui ! Ne soyez pas surpris ! L'action create est une route de type list même si vous ne manipulez qu'une seule ressource car il n'y a pas (besoin) d'identifiant de ressource dans l'url POST /<PREFIX>/.

Pendant la détection des routes, ces routes standard sont simplement ajoutées dans la liste des routes possibles. Par la suite, le routeur vérifie si les actions sont implémentées pour construire leurs motifs d'url.

Dans l'exemple précédent, le Viewset :

class BookViewset(ListModelMixin, RetrieveModelMixin, UpdateModelMixin,
                  GenericViewSet):
    queryset = Book.objects.all()
    serializer_class = BookSerializer

implémente par le biais des mixins les méthodes (actions) :

  • list (ListModelMixin)
  • retrieve (RetrieveModelMixin)
  • update et partial_update (UpdateModelMixin)

Donc quand on demande au routeur de générer les motifs d'urls :

router = DefaultRouter()
router.register(r'books', BookViewset)
urlpatterns = router.urls

on obtient :

  • GET /books/ (list)
  • GET /books/<pk>/ (retrieve)
  • PUT /books/<pk>/ (update)
  • PATCH /books/<pk>/ (partial_update)

Astuce: pour lister les routes disponibles, vous pouvez en phase de développement installer l'application Django Extension et exécuter la commande show_urls. Les urls sont listées, mais aussi leur emplacement et surtout leur nom. Par contre, vous n'avez pas les méthodes HTTP (normal, ca n'est pas un outil de DRF). Attention, démonstration:

/   rest_framework.routers.APIRoot  api-root
/.<format>/ rest_framework.routers.APIRoot  api-root
/books.<format>/    books.urls.BookViewset  book-list
/books/     books.urls.BookViewset  book-list
/books/<pk>.<format>/       books.urls.BookViewset  book-detail
/books/<pk>/        books.urls.BookViewset  book-detail

Les routes dynamiques

En plus des actions standard, on peut définir ses propres actions. Pour que ces actions soient routées, on utilise les routes dynamiques. Elles sont aussi de type list ou detail. Pour définir des actions dynamiques, il existe les décorateurs list_route et detail_route. Ces décorateurs acceptent des paramètres pour :

  • définir les méthodes HTTP acceptées (oui, plusieurs sont possibles)
  • surcharger les attributs de la classe Viewset (tous sans exception, alors pas de pitié !)
  • modifier l'url et le nom du motif d'url avec le paramètre url_path. Par défaut, c'est le nom de la méthode décorée qui est utilisé pour construire ces éléments.

Voici l'exemple d'un Viewset qui a décidé un bon matin d'avoir deux actions dynamiques :

from django.db import models

from rest_framework import serializers
from rest_framework.decorators import list_route, detail_route
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.routers import DefaultRouter
from rest_framework.viewsets import GenericViewSet


class Book(models.Model):
    pass


class BookSerializer(serializers.ModelSerializer):
    class Meta:
        model = Book


class BookViewset(GenericViewSet):
    queryset = Book.objects.all()
    serializer_class = Book
    permission_classes = (IsAuthenticated,)

    @detail_route(methods=['get'])
    def my_custom_detail_action(self, request, *args, **kwargs):
        pass  # Do some stuff

    @list_route(methods=['post'], permission_classes=(AllowAny,),
                url_path='a-better-sexy-name')
    def my_custom_list_action(self, request, *args, **kwargs):
        pass  # Do some stuff

router = DefaultRouter()
router.register(r'books', BookViewset)
urlpatterns = router.urls

Pour chacune de ces actions, on retrouve les caractéristiques des deux namedtuples DynamicListRoute et DynamicDetailRoute que l'on retrouve eux aussi dans l'attribut routes de la classe SimpleRouter:

DynamicListRoute(
    url=r'^{prefix}/{methodname}{trailing_slash}$',
    name='{basename}-{methodnamehyphen}',
    initkwargs={}
)

DynamicDetailRoute(
    url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$',
    name='{basename}-{methodnamehyphen}',
    initkwargs={}
)

La détection des actions dynamiques dans la méthode get_routes() nécessite un peu plus d'efforts au routeur. Quand on utilise un des deux décorateurs, ce dernier va ajouter des attributs aux méthodes décorées :

  • bind_to_methods: c'est le paramètre methods du décorateur : c'est la liste des méthodes HTTP acceptées (get par défaut). À noter qu'il est inutile d'hurler pour fournir ce paramètre, il sera forcé en minuscules.
  • detail: c'est un flag qui indique si la route est de type detail ou list. "Nan sans déconner" ? Encore plus incroyable, ce flag est mis tout seul en fonction du décorateur detail_route ou list_route appellé ! Ralala, ils ont pensé à tout !
  • kwargs: celui-là par contre est moins évident. "Bah, c'est un kwargs fourre-tout quoi" ! Eh bien non, c'est même tout à fait normal. Il s'agit de tous les attributs supplémentaires passés au décorateur qui devront surcharger ceux du Viewset. Bon ok, à part deux/trois exceptions sinon c'est pas drôle. En interne, il sera combiné au dictionnaire initkwargs. Puis il sera fourni à la fonction génératrice as_view() du Viewset. Enfin, cette fonction génératrice pourra créer des instances de classe de vues avec ces paramètres qui écraseront les attributs de classes héritées. Ça y est, le mystère du initkwargs est levé.

Quand le routeur inspecte les méthodes implémentées du Viewset, il cherche ces attributs pour déterminer si ce sont des routes dynamiques. Si c'est le cas, ces attributs lui permettront de construire un nouveau namedtuple Route avec les attributs adéquats. C'est ainsi que la route dynamique devient finalement une splendide route classique. La nature est bien faite.

Revenons à notre exemple et voyons ce que l'on obtient une fois que le routeur a effectué ce travail de résolution pour une route dynamique de type detail (en list c'est pareil mais il n'y a pas de lookup). Accrochez-vous, on y va en détail.

L'attribut url=r'^{prefix}/{lookup}/{methodname}{trailing_slash}$' une fois résolu se décompose en :

  • {prefix} = books, via l'attribut prefix de la méthode register du routeur (soit ici router.register(r'books', BookViewset))
  • {lookup} = (?P<pk>[^/.]+), je vous épargne les détails mais sachez que vous pouvez modifier ce pattern en jouant avec les attributs de la class Viewset:
    • lookup_prefix
    • lookup_url_kwarg
    • lookup_value
  • {methodname} = le nom de la méthode d'action (sans modification quelconque). Ce nom de méthode peut être surchargé avec le paramètre url_path des décorateurs. Ce que l'on fait dans cet exemple pour avoir un motif d'url définitivement plus beau gosse.
  • {trailing_slash} = /. Vous pouvez changer vos habitudes de Djangonaute et désactiver le trailing slash à la création de l'instance du routeur. Exemple : DefaultRouter(trailing_slash=False)

Ce qui donne respectivement les motifs d'urls suivants :

  • GET /books/(?P<pk>[^/.]+)/my_custom_detail_action/
  • POST /books/a-better-sexy-name/.

Pour l'attribut name='{basename}-{methodnamehyphen}' :

  • {basename} = book. Il s'agit en fait du nom de la classe modèle Book en minuscule via l'attribut queryset de la classe Viewset. Si cet attribut queryset n'existe pas, alors il doit être fourni à la fonction register du routeur avec le paramètre base_name. Concrètement, vous devriez faire : router.register(r'books', BookViewset, base_name='book'). Sinon c'est coups de têtes/balayettes ! Bah oui, DRF fait pas encore le café ;-)
  • {methodnamehyphen} = nom de la méthode, en remplaçant les underscores par des tirets. Idem, ce nom de méthode peut être surchargé avec le paramètre url_path

Ce qui donne respectivement les noms de motifs d'urls suivants:

  • book-my-custom-detail-action
  • book-a-better-sexy-name

Bref, tout ça pour vous dire que le premier point d'entrée (my_custom_detail_action) défini une action de plus de type route en GET (avec identifiant). Il hérite du comportement par défaut défini par les attributs de classe du Viewset et du routeur. Le second point d'entrée (my_custom_list_action) défini une autre action de type list en POST (pas d'identifiant). Néanmoins, celui-ci est accessible par tous.

Pour finir, dans cet article, on vous vend les mérites des Viewsets contre les API Views. C'est en grande partie vrai grâce aux routes dynamiques. Au lieu de définir de nouvelles vues d'API View parce qu'elles n'existent pas en tant que route standard dans un Viewset, vous pouvez les déclarer comme méthodes du Viewset grâce aux routes dynamiques. Ainsi, vous bénéficiez de tous les avantages des Viewset. Qu'est-ce qu'on dit ? Merci les routes dynamiques !

À venir

Ce ne fut pas sans douleur que vous êtes devenu un maître routeur aguérri. Rassurez-vous, on a exploré un des aspects les plus complexes de DRF. Les routeurs n'ayant désormais plus de secret pour vous, on verra dans un prochain article comment les intégrer avec Django.

ABONNEZ-VOUS À LA NEWSLETTER !
Voir aussi
La Haute-Garonne met à l’honneur l’application Geotrek dans son magazine 08/06/2021

Le magazine Haute-Garonne du mois de mai-juin 2021 présente le site marando.haute-garonne.fr

Comment migrer vers une version récente de Django ? Comment migrer vers une version récente de Django ? 15/04/2021

Que ce soit pour avoir les dernières fonctionnalités ou les correctifs de sécurité, rester sur ...

Créer un tag d'inclusion avec paramètres dans Django 22/12/2020

La bibliothèque de tags interne permet d'enregistrer des tags avec paramètres ou des tags ...

Présentation de django-admin-watchdog Présentation de django-admin-watchdog 12/11/2020

Comment garder une trace des erreurs Django en toute simplicité.

Présentation de django-tracking-fields Présentation de django-tracking-fields 03/11/2020

Suivi de modification d'objets Django