DRF est une des perles de Django. De Python même. Comme marshmallow, requests, jupyter, pandas, SQLAlchemy ou l’admin Django. Python a tellement d’outils 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_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 ?
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 seule, 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 (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…
En overridant la get_serializer_class on a un truc quand même propre non ?
def get_serializer_class(self):
serializer_class = self.serializer_class
Nan j’ai tenté mais ça marche pas :( Quand tu fais un post, tu veux poster un Id, mais du veux que le résultat retourné soit un objet.
Petite typo sur le
max_length
:name = models.CharField(max_lengt=64)
Glop
Ha bah.. Il tombe à pic cet article..
Je m’embête depuis ce matin à trouver un truc propre, j’étais parti sur la solution 1, mais effectivement, refaire l’écriture à la main, c’est pas la joie.
Merci pour l’astuce.
Pour info, comme soumis dans le ticket, nous utilisons cette solution :
Bon comme ça ne marchait pas directement chez moi, je poste la version “corrigée”:
D’un côté dans un
helpers.py
:from collections import OrderedDict
from rest_framework import serializers
class AsymetricRelatedField(serializers.PrimaryKeyRelatedField):
Et dans
serializers.py
:class FooSerializer(serializers.ModelSerializer):
Il faut que tu précise ce qui ne marchait pas et ce que tu as corrigé.
Oui, j’ajoute de l’information alors ;-)
Les corrections sont au niveau de la méthode
from_serializer
et de l’appel dans le serializer.Dans la méthode
from_serializer
, pour respecter la méthode type, il faut que le 2ème argument soit un tuple:[cls] -> (cls,)
Comme on passe une classe et non une instance, on ne peut pas faire
serializer.class.name
mais plutôtserializer.__name__
Enfin au niveau du serializer, on utilise la méthode de classe
from_serializer
à laquelle on passe la classe du serializer à utiliser, ce qui nous donne une nouvelle classe de laquelle on appelle le constructeur:bar = AsymetricRelatedField.from_serializer(BarSerializer)()
En tout cas, ce que je n’avais pas mis dans mon premier post, l’idée est vraiment très bonne ! En plus elle tombe à pic et solutionne un problème que j’avais la veille.
Dommage que la proposition ne soit pas passée et même si je comprend les raisons, on fait bien ce qu’on veut avec nos APIS !
Encore un bon article donc :-)
C’ est quoi ta methodologie quand tu t’attaques à un probleme costaud ?
Tu passes simplement ta paume sur la doc à la Matrix ?
Ou tu fais des dessins au crayon, ou tu utilises un UML generator pour visualiser vite fait la tuyauterie ?
quand je regardes des codes sources sur nullege, arrivé en bas du fichier, ma mémoire et déjà saturée depuis 5 minutes… Alors pour trouver un hack , jte racontes pas le fossé.
Combien de temps ça t’a pris à cogiter pour ce cas en particulier ?
Bonjour,
Trop bien cet article.
J’ai un truc a faire depuis des mois et je m’y suis pas encore attelé.. surement car j’ai pas encore la BONNE solution.
J’ai un point API json_rcp assez sale et verbeux auquel je peux pas toucher, car utilisé dans une autre condition.
Je veux donc faire un bridge API json_rpc qui expose des methodes faisant intervenir plusieurs appel json, changeant la structure et filtrant les data.
je me demande quel est le top framework REST pour faire ca.. Car les solutions que j’ai trouvé sont assez dependante de l’ORM
C’est certes pratique d’avoir ce petit helper dans le projet mais la solution de Fabs me semble simple et efficace.
Je ne fais plus trop de Django depuis un moment mais sur mes projets Flask + marshmallow je définis aussi mon schema (~serializer) avec des champs en dump_only et d’autres en read_only. T’es pas tant que ça une grosse feignasse vu comme tu te fais chier à aller fouiner le code source pour faire ta tambouille. T’es surtout un bon geek curieux, un macgyver du Python web :-)
Faut vraiment que je sois bloqué par les limites du package ou que la doc soit inexistantes pour en arriver là. Bon c’est vrai, ça arrive souvent avec les packages pour flask.
@buffalo974 : crayon et papier d’abord :)
Après lire le code source d’un projet externe ça demande vraiment d’être expérimenté, faut pas déconner. Souvent on entend des gens dire “lisez les sources ça fait progresser”, mais y a un fossé entre pouvoir jeter un coup d’oeil à un fichier et comprendre toutes les implications dans un projet. Pour ce dernier, il faut avoir codé pas mal de projets soi-même.
Pour ce cas là ça m’a pris une bonne demi-heure à une heure. Je savais déjà ce que je voulais faire car j’avais cherché longtemps un moyen de le faire. Donc j’ai juste regardé le code source du truc qui me le permettait pas, et j’ai tenté de le modifier jusqu’à ce que ça marche dans un test unitaire à l’arrache. La tricherie c’est que DRF utilise pas mal de design patterns que je connais bien, et ça change tout car quand je le vois faire quelque chose, ça me parle. D’autant que j’avais déjà regardé le code source de DRF dans des missions précédents et ce n’était pas une base non familière.
@lollo : DRF marchera très bien pour ça, on peut tout faire sans ORM. Mais vue que tu fais un sous appel à un autre système derrière pour des raisons de perfs tu voudras peut être plutôt taper dans un framework asynchrone. Cherche un micro framework tout simple si c’est le cas. L’avantage de DRF c’est qu’il gère toute l’authentification, et c’est pas rien sur une API. Le désavantage c’est que c’est un gros morceau.
@Pyglouthon : c’est pas faux :)