web-dev-qa-db-ger.com

DRF: Einfache Fremdschlüsselzuordnung mit verschachtelten Serialisierern?

Mit Django REST Framework ermöglicht ein standardmäßiger ModelSerializer die Zuweisung oder Änderung von ForeignKey-Modellbeziehungen durch POSTing einer ID als Ganzzahl.

Was ist der einfachste Weg, um dieses Verhalten aus einem verschachtelten Serializer herauszuholen?

Beachten Sie, ich spreche nur über das Zuweisen vorhandener Datenbankobjekte, nicht geschachtelte Erstellung.

Ich habe mich in der Vergangenheit mit zusätzlichen 'id'-Feldern im Serializer und mit benutzerdefinierten create- und update-Methoden herumgehackt, aber dies ist ein scheinbar einfaches und häufiges Problem für mich, dass ich neugierig bin, den besten Weg zu kennen.

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

Die beste Lösung ist es, zwei verschiedene Felder zu verwenden: einen zum Lesen und einen zum Schreiben. Ohne heavy heben zu müssen, ist es schwierig, das zu finden, wonach Sie in einem einzigen Feld suchen.

Das schreibgeschützte Feld wäre Ihr verschachtelter Serialisierer (in diesem Fall ChildSerializer), und Sie können dieselbe geschachtelte Darstellung erhalten, die Sie erwarten. Die meisten Leute definieren dies nur als child, da ihr Frontend bereits an diesem Punkt geschrieben wurde und eine Änderung zu Problemen führen würde.

Das schreibgeschützte Feld wäre eine PrimaryKeyRelatedField , die Sie normalerweise zum Zuweisen von Objekten anhand ihres Primärschlüssels verwenden würden. Dies muss nicht nur schreibgeschützt sein, vor allem wenn Sie versuchen, eine Symmetrie zwischen dem Empfangenen und dem Gesendeten zu erreichen, aber es klingt so, als würde Ihnen dies am besten zusagen. In diesem Feld sollte eine source auf das Fremdschlüsselfeld (in diesem Beispiel child) gesetzt sein, damit es bei der Erstellung und Aktualisierung ordnungsgemäß zugewiesen wird.


Dies wurde einige Male in der Diskussionsgruppe angesprochen, und ich denke, dass dies immer noch die beste Lösung ist. Danke an Sven Maurer für den Hinweis .

34
Kevin Brown

Hier ist ein Beispiel, worüber Kevins Antwort spricht, wenn Sie diesen Ansatz verwenden und zwei separate Felder verwenden möchten.

In Ihrem 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)

dann 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

Wenn Sie source=child festlegen, kann child_id als untergeordnetes Element fungieren, wenn es nicht überschrieben würde (unser gewünschtes Verhalten). write_only=True macht child_id für das Schreiben verfügbar, verhindert jedoch, dass es in der Antwort angezeigt wird, da die ID bereits im ChildSerializer angezeigt wird

30
joslarson

Die Verwendung zweier verschiedener Felder wäre ok (als @ Kevin Brown und @joslarson ), aber ich denke, es ist nicht perfekt (mir). Das Abrufen von Daten von einem Schlüssel (child) und das Senden von Daten an einen anderen Schlüssel (child_id) kann für Front-End -Entwickler ein wenig mehrdeutig sein. (überhaupt keine Beleidigung)


Ich schlage hier also vor, überschreiben Sie die to_representation() -Methode von ParentSerializer, um die Aufgabe zu erledigen.

def to_representation(self, instance):
    response = super().to_representation(instance)
    response['child'] = ChildSerializer(instance.child).data
    return response



Vollständige Darstellung des Serializers

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



Vorteil dieser Methode?

Bei Verwendung dieser Methode brauchen wir nicht zwei separate Felder zum Erstellen und Lesen. Hier kann sowohl das Erstellen als auch das Lesen mit der Taste child erfolgen.


Beispiel-Payload zum Erstellen von parent Instanz

{
        "name": "TestPOSTMAN_name",
        "phone_number": 1,
        "child": 1
    }



Bildschirmfoto
 POSTMAN screenshot

11
JPG

Es gibt eine Möglichkeit, ein Feld beim Erstellen/Aktualisieren zu ersetzen:

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

So habe ich dieses Problem gelöst.

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)

Sie übergeben Ihr verschachteltes untergeordnetes Serialisierungsprogramm genau so, wie Sie es vom Serialisierungsprogramm erhalten, dh als Json/Dictionary. In to_internal_value instanziieren wir das untergeordnete Objekt, wenn es eine gültige ID hat, damit DRF das Objekt weiter bearbeiten kann.

2
Gaurav Butola

Ein paar Leute hier haben einen Weg platziert, um ein Feld zu behalten, aber sie können immer noch die Details abrufen, wenn sie das Objekt abrufen und es nur mit der ID erstellen. Ich habe etwas mehr generische Implementierung vorgenommen, wenn die Leute interessiert sind:

Zuerst die 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)

Dann die Klasse selbst:

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

Die Verwendung ist so, wenn Sie irgendwo einen Serializer haben:

class YourSerializer(ModelSerializer):
    something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)

Auf diese Weise können Sie ein Objekt mit einem Fremdschlüssel nur noch mit dem PK erstellen, das vollständige serialisierte geschachtelte Modell wird jedoch zurückgegeben, wenn Sie das erstellte Objekt abrufen (oder wann immer).

2
Bono

Ich denke, der von Kevin skizzierte Ansatz wäre wahrscheinlich die beste Lösung, aber ich konnte es nie schaffen, dass es funktioniert. DRF hat immer wieder Fehler geworfen, wenn ich sowohl einen verschachtelten Serialisierer als auch ein Primärschlüsselfeldset hatte. Das Entfernen des einen oder des anderen würde funktionieren, brachte mir aber offensichtlich nicht das Ergebnis, das ich brauchte. Das Beste, was ich mir vorstellen kann, ist das Erstellen von zwei verschiedenen Serialisierern zum Lesen und Schreiben.

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

Dafür gibt es ein Paket! Schauen Sie sich PresentablePrimaryKeyRelatedField im Drf Extra Fields-Paket an.

https://github.com/Hipo/drf-extra-fields

0
Yiğit Güler