web-dev-qa-db-fra.com

DRF: assignation simple de clé étrangère avec des sérialiseurs imbriqués?

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
38
John Rork

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é .

34
Kevin Brown

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

30
joslarson

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
    }



Capture d'écran
 POSTMAN screenshot

11
JPG

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
3
Anton Dmitrievsky

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.

2
Gaurav Butola

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).

2
Bono

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
1
jayarnielsen