Accepter un ID mais retourner un objet pour les liens de Django Rest Framework 2 Recently updated !
jeudi 8 juin 2017 à 09:50DRF est une des perles de Django. De Python même. Comme marshmallow, requests, jupyter, pandas, SQLAlchemy ou l’admin Django. Python a tellement de libs extraordinaires.
Mais aucune n’est parfaite, et une chose qui m’a toujours emmerdé avec celle-ci, c’est que si j’ai un modèle du genre:
class Foo(models.Model): name = models.CharField(max_lengt=64) bar = models.ForeignKey(Bar) |
Et le serializer:
class FooSerialize(serilizers.ModelSerializer): class Meta: model = Foo |
J’ai le choix entre soit avoir que des ID…
En lecture (chiant) :
GET /api/foos/1/ { name: "toto", bar: 2 }
Et en écriture (pratique) :
POST /api/foos/ { name: "tata", bar: 2 }
Soit avoir que des objets.
En lecture (pratique):
GET /api/foos/1/ { name: "toto", bar: { // tout l'objet bar disponible en lecture } }
Et en écriture (chiant) : POST /api/foos/ { name: "tata", bar: { // tout l'objet bar à se taper à écrire } }
Il y a aussi la version hypermedia où l’id est remplacé par une URL. Mais vous voyez le genre : mon API REST est soit pratique en lecture mais relou à écrire, soit pratique en écriture (je fournis juste une référence), mais relou en lecture, puisque je dois ensuite fetcher chaque référence.
GraphQL répond particulièrement bien à ce problème, mais bon, la techno est encore jeune, et il y a encore plein d’API REST à coder pour les années à venir.
Comment donc résoudre ce casse-tête, Oh Sam! – sauveur de la pythonitude ?
Solution 1, utiliser un serializer à la place du field
class FooSerializer(serilizers.ModelSerializer): bar = BarSerializer() class Meta: model = Foo |
Et là j’ai bien l’objet complet qui m’est retourné. Mais je suis en lecture seul, et il faut que je fasse l’écriture à la main. Youpi.
Pas la bonne solution donc.
Solution 2, écrire deux serializers
Ben ça marche mais il faut 2 routings, ça duplique l’API, la doc, les tests. Moche. Next.
Solution 3, un petit hack
En lisant le code source de DRF (ouai j’ai conscience que tout le monde à pas la foi de faire ça), j’ai noté que ModelSerializer
génère automatiquement pour les relations un PrimaryKeyRelatedField
, qui lui même fait le lien via l’ID. On a des classes similaires pour la version full de l’objet et celle avec l’hyperlien.
En héritant de cette classe, on peut créer une variante qui fait ce qu’on veut:
from collections import OrderedDict from rest_framework import serializers class AsymetricRelatedField(serializers.PrimaryKeyRelatedField): # en lecture, je veux l'objet complet, pas juste l'id def to_representation(self, value): return self.serializer_class(value).data # petite astuce perso et pas obligatoire pour permettre de taper moins # de code: lui faire prendre le queryset du model du serializer # automatiquement. Je suis lazy def get_queryset(self): if self.queryset: return self.queryset return self.serializer_class.Meta.model.objects.all() # Get choices est utilisé par l'autodoc DRF et s'attend à ce que # to_representation() retourne un ID ce qui fait tout planter. On # réécrit le truc pour utiliser item.pk au lieu de to_representation() def get_choices(self, cutoff=None): queryset = self.get_queryset() if queryset is None: return {} if cutoff is not None: queryset = queryset[:cutoff] return OrderedDict([ ( item.pk, self.display_value(item) ) for item in queryset ]) # DRF saute certaines validations quand il n'y a que l'id, et comme ce # n'est pas le cas ici, tout plante. On desactive ça. def use_pk_only_optimization(self): return False # Un petit constructeur pour générer le field depuis un serializer. lazy, # lazy, lazy... @classmethod def from_serializer(cls, serializer, name=None, args=(), kwargs={}): if name is None: name = f"{serializer.__class__.name}AsymetricAutoField" return type(name, [cls], {"serializer_class": serializer}) |
Et du coup:
class FooSerializer(serializers.ModelSerializer): bar = AsymetricRelatedField(BarSerializer) class Meta: model = Foo |
Et voilà, on peut maintenant faire:
GET /api/foos/1/ { name: "toto", bar: { // tout l'objet bar disponible en lecture } } POST /api/foos/ { name: "tata", bar: 2 }
Elle est pas belle la vie ?
Ca serait bien cool que ce soit rajouté officiellement dans DRF tout ça. Je crois que je vais ouvrir un ticket…