Avec Django REST Framework, un ModelSerializer standard permet d’attribuer ou de modifier les relations de modèle ForeignKey en postant un ID sous forme d’entier.
Quel est le moyen le plus simple d’obtenir ce comportement d’un sérialiseur imbriqué?
Notez que je ne parle que d’affecter des objets de base de données existants, pas création imbriquée.
Auparavant, j’ai corrigé cela avec des champs "id" supplémentaires dans le sérialiseur et des méthodes personnalisées create
et update
, mais c’est un problème apparemment si simple et si fréquent que je suis curieux de connaître le meilleur moyen.
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# phone_number relation is automatic and will accept ID integers
children = ChildSerializer() # this one will not
class Meta:
model = Parent
La meilleure solution ici consiste à utiliser deux champs différents: l’un pour la lecture et l’autre pour l’écriture. Sans faire lourd levage, il est difficile d'obtenir ce que vous cherchez dans un seul champ.
Le champ en lecture seule serait votre sérialiseur imbriqué (ChildSerializer
dans ce cas) et vous permettra d'obtenir la même représentation imbriquée que celle que vous attendez. La plupart des gens définissent cela comme étant simplement child
, car ils ont déjà leur interface écrite à ce point et son changement poserait des problèmes.
Le champ en écriture seule serait PrimaryKeyRelatedField
, ce que vous utiliseriez généralement pour affecter des objets en fonction de leur clé primaire. Cela ne doit pas nécessairement être en écriture, en particulier si vous essayez d’opérer une symétrie entre ce qui est reçu et ce qui est envoyé, mais cela semble pouvoir vous convenir le mieux. A source
doit être défini sur le champ Clé étrangère (child
dans cet exemple) pour qu'il soit correctement attribué lors de la création et de la mise à jour.
Cela a été évoqué à quelques reprises par le groupe de discussion, et je pense que c'est toujours la meilleure solution. Merci à Sven Maurer de l'avoir signalé .
Voici un exemple de la réponse de Kevin, si vous souhaitez adopter cette approche et utiliser 2 champs distincts.
Dans votre models.py ...
class Child(models.Model):
name = CharField(max_length=20)
class Parent(models.Model):
name = CharField(max_length=20)
phone_number = models.ForeignKey(PhoneNumber)
child = models.ForeignKey(Child)
alors serializers.py ...
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
# if child is required
child = ChildSerializer(read_only=True)
# if child is a required field and you want write to child properties through parent
# child = ChildSerializer(required=False)
# otherwise the following should work (untested)
# child = ChildSerializer()
child_id = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all(), source='child', write_only=True)
class Meta:
model = Parent
Définir source=child
permet à child_id d'agir comme un enfant si, par défaut, il n'était pas remplacé (notre comportement souhaité). write_only=True
rend child_id disponible pour l'écriture, mais l'empêche de s'afficher dans la réponse puisque l'id apparaît déjà dans ChildSerializer
Ok (comme @Kevin Brown et @ joslarson mentionne) serait utilisé, mais je pense que ce n'est pas parfait (pour moi). Parce qu'obtenir des données d'une clé (child
) et les envoyer à une autre clé (child_id
) peut paraître un peu ambigu pour les développeurs {FRONT-END}. (aucune infraction du tout)
Donc, ce que je suggère ici, c'est de remplacer la méthode to_representation()
de ParentSerializer
fera l'affaire.
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Représentation complète du sérialiseur
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
fields = '__all__'
class ParentSerializer(ModelSerializer):
class Meta:
model = Parent
fields = '__all__'
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Avantage de cette méthode?
En utilisant cette méthode, nous n’avons pas besoin de deux champs distincts pour la création et la lecture. Ici, la création et la lecture peuvent être effectuées à l'aide de la touche child
.
Exemple de charge utile pour créer une instance parent
{
"name": "TestPOSTMAN_name",
"phone_number": 1,
"child": 1
}
Il existe un moyen de substituer un champ à l'opération de création/mise à jour:
class ChildSerializer(ModelSerializer):
class Meta:
model = Child
class ParentSerializer(ModelSerializer):
child = ChildSerializer()
# called on create/update operations
def to_internal_value(self, data):
self.fields['child'] = serializers.PrimaryKeyRelatedField(
queryset=Child.objects.all())
return super(ParentSerializer, self).to_internal_value(data)
class Meta:
model = Parent
Voici comment j'ai résolu ce problème.
serializers.py
class ChildSerializer(ModelSerializer):
def to_internal_value(self, data):
if data.get('id'):
return get_object_or_404(Child.objects.all(), pk=data.get('id'))
return super(ChildSerializer, self).to_internal_value(data)
Vous allez simplement transmettre votre sérialiseur enfant imbriqué tel que vous l'avez obtenu du sérialiseur, c'est-à-dire enfant sous la forme d'un dictionnaire/dictionnaire Dans to_internal_value
, nous instancions l'objet enfant s'il possède un ID valide afin que DRF puisse continuer à utiliser cet objet.
Quelques personnes ici ont mis en place un moyen de conserver un champ, tout en pouvant obtenir les détails lors de la récupération de l'objet et le créer avec uniquement l'ID. J'ai fait une implémentation un peu plus générique si les gens sont intéressés:
Tout d'abord les tests:
from rest_framework.relations import PrimaryKeyRelatedField
from Django.test import TestCase
from .serializers import ModelRepresentationPrimaryKeyRelatedField, ProductSerializer
from .factories import SomethingElseFactory
from .models import SomethingElse
class TestModelRepresentationPrimaryKeyRelatedField(TestCase):
def setUp(self):
self.serializer = ModelRepresentationPrimaryKeyRelatedField(
model_serializer_class=SomethingElseSerializer,
queryset=SomethingElse.objects.all(),
)
def test_inherits_from_primary_key_related_field(self):
assert issubclass(ModelRepresentationPrimaryKeyRelatedField, PrimaryKeyRelatedField)
def test_use_pk_only_optimization_returns_false(self):
self.assertFalse(self.serializer.use_pk_only_optimization())
def test_to_representation_returns_serialized_object(self):
obj = SomethingElseFactory()
ret = self.serializer.to_representation(obj)
self.assertEqual(ret, SomethingElseSerializer(instance=obj).data)
Puis la classe elle-même:
from rest_framework.relations import PrimaryKeyRelatedField
class ModelRepresentationPrimaryKeyRelatedField(PrimaryKeyRelatedField):
def __init__(self, **kwargs):
self.model_serializer_class = kwargs.pop('model_serializer_class')
super().__init__(**kwargs)
def use_pk_only_optimization(self):
return False
def to_representation(self, value):
return self.model_serializer_class(instance=value).data
L’utilisation est la même, si vous avez un sérialiseur quelque part:
class YourSerializer(ModelSerializer):
something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)
Cela vous permettra de créer un objet avec une clé étrangère toujours avec la clé PK, mais renverra le modèle imbriqué sérialisé complet lors de la récupération de l'objet que vous avez créé (ou chaque fois que vous en avez réellement).
Je pense que l'approche décrite par Kevin serait probablement la meilleure solution, mais je ne pouvais jamais la faire fonctionner. DRF continuait de générer des erreurs lorsque j'avais à la fois un sérialiseur imbriqué et un jeu de champs de clé primaire. Supprimer l'un ou l'autre fonctionnerait, mais de toute évidence, il ne m'a pas donné le résultat dont j'avais besoin. Le mieux que je puisse trouver est de créer deux sérialiseurs différents pour la lecture et l’écriture, comme si ...
sérialiseurs.py:
class ChildSerializer(serializers.ModelSerializer):
class Meta:
model = Child
class ParentSerializer(serializers.ModelSerializer):
class Meta:
abstract = True
model = Parent
fields = ('id', 'child', 'foo', 'bar', 'etc')
class ParentReadSerializer(ParentSerializer):
child = ChildSerializer()
views.py
class ParentViewSet(viewsets.ModelViewSet):
serializer_class = ParentSerializer
queryset = Parent.objects.all()
def get_serializer_class(self):
if self.request.method == 'GET':
return ParentReadSerializer
else:
return self.serializer_class