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_length=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 ?
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 seule, et il faut que je fasse l’écriture à la main. Youpi.
Pas la bonne solution donc.
Ben ça marche mais il faut 2 routings, ça duplique l’API, la doc, les tests. Moche. Next.
En lisant le code source de DRF (ouais 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):
# le self.serializer_class.serializer_class est redondant
# mais obligatoire
return self.serializer_class.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.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 désactive ç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})(*args, **kwargs)
Et du coup:
class FooSerializer(serializers.ModelSerializer):
bar = AsymetricRelatedField.from_serializer(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…
]]>