DRF: Einfache Fremdschlüsselzuweisung mit verschachtelten Serialisierern?

74

Mit dem Django REST Framework ermöglicht ein Standard-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?

Hinweis: Ich spreche nur über das Zuweisen vorhandener Datenbankobjekte, nicht über die verschachtelte Erstellung.

Ich habe um diesen in der Vergangenheit mit zusätzlichen ‚id‘ Feldern in dem Serializer und mit benutzerdefinierter gehackt weg createund updateMethoden, aber das ist so ein scheinbar einfaches und häufiges Problem für mich , dass ich neugierig bin der beste Weg , zu wissen.

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

Antworten:

53

Die beste Lösung besteht darin, zwei verschiedene Felder zu verwenden: eines zum Lesen und das andere zum Schreiben. Ohne schweres Heben ist es schwierig, auf einem einzigen Feld das zu finden, wonach Sie suchen .

Das schreibgeschützte Feld ist ( ChildSerializerin diesem Fall) Ihr verschachtelter Serializer und ermöglicht es Ihnen, dieselbe verschachtelte Darstellung zu erhalten, die Sie erwarten. Die meisten Leute definieren dies als gerecht child, weil sie ihr Front-End bereits zu diesem Zeitpunkt geschrieben haben und eine Änderung Probleme verursachen würde.

Das schreibgeschützte Feld ist a PrimaryKeyRelatedField. Dies ist das, was Sie normalerweise zum Zuweisen von Objekten basierend auf ihrem Primärschlüssel verwenden würden. Dies muss nicht nur schreibgeschützt sein, insbesondere wenn Sie versuchen, eine Symmetrie zwischen dem, was empfangen und gesendet wird, anzustreben, aber es klingt so, als würde dies am besten zu Ihnen passen. Für dieses Feld sollte einsource Satz für das Fremdschlüsselfeld ( childin diesem Beispiel) festgelegt sein, damit es beim Erstellen und Aktualisieren ordnungsgemäß zugewiesen wird.


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

Kevin Brown
quelle
Kevin, danke für deine Antwort. Ich hatte mit der gleichen Art von Problem zu kämpfen. Ich habe ChildSerializer zwei Felder hinzugefügt. parent = ParentSerializer (read_only = True) und parent_id = serializers.PrimaryKeyRelatedField (...., write_only = True, ....) Ich habe auch parent und parent_id zu Feldern des ChildSerializer hinzugefügt. In der Antwort wird jedoch kein Feld child_id angezeigt . Was ist eigentlich gut und praktisch, aber ich frage mich, was der Grund dafür ist? Hast du irgendeine Idee?
Hnroot
gute Antwort. Es fehlt nur ein Beispielcode wie in Skinnys Antwort (wahrscheinlich unten)
Molecular
70

Aktualisiert am 05. Juli 2020

Dieser Beitrag erhält mehr Aufmerksamkeit und zeigt an, dass mehr Menschen eine ähnliche Situation haben. Deshalb habe ich beschlossen, einen generischen Weg hinzuzufügen , um dieses Problem zu lösen. Diese generische Methode eignet sich am besten für Sie, wenn Sie mehr Serializer haben, die in dieses Format geändert werden müssen.

Da DRF diese Funktionalität nicht sofort bereitstellt, müssen wir zuerst ein Serializer-Feld erstellen .

from rest_framework import serializers


class RelatedFieldAlternative(serializers.PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.serializer = kwargs.pop('serializer', None)
        if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
            raise TypeError('"serializer" is not a valid serializer class')

        super().__init__(**kwargs)

    def use_pk_only_optimization(self):
        return False if self.serializer else True

    def to_representation(self, instance):
        if self.serializer:
            return self.serializer(instance, context=self.context).data
        return super().to_representation(instance)

Ich bin nicht sehr beeindruckt von diesem Klassennamen RelatedFieldAlternative, Sie können alles verwenden, was Sie wollen. Verwenden Sie dann dieses neue Serializer-Feld in Ihrem übergeordneten Serializer als:

class ParentSerializer(ModelSerializer):
   child = RelatedFieldAlternative(queryset=Child.objects.all(), serializer=ChildSerializer)

    class Meta:
        model = Parent
        fields = '__all__'

Ursprünglicher Beitrag

Die Verwendung von zwei verschiedenen Bereichen wäre ok (wie @ Kevin Brown und @joslarson erwähnt), aber ich denke , es ist nicht perfekt (für mich). Da das Abrufen von Daten von einem Schlüssel ( child) und das Senden von Daten an einen anderen Schlüssel ( child_id) für Front-End- Entwickler möglicherweise etwas mehrdeutig ist . (überhaupt keine Beleidigung)


Also, was ich hier vorschlage, ist, die to_representation()Methode zu überschreiben, mit der ParentSerializerder Job erledigt wird.

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?

Mit dieser Methode benötigen wir keine zwei separaten Felder zum Erstellen und Lesen. Hier können sowohl das Erstellen als auch das Lesen mit dem child Schlüssel erfolgen.


Beispielnutzlast zum Erstellen einer parentInstanz

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



Bildschirmfoto
POSTMAN Screenshot

JPG
quelle
8
Ich habe seit Tagen nach einer solchen Antwort gesucht. Die Einfachheit ist wunderschön. +1 sicher.
Bigbob556677
3
Dies ist eine elegante Lösung, hat jedoch einige Nachteile. Der Schema-Generator von drf kann den Nest-Serializer nicht erkennen, daher würde das Schema das Feld nur als PrimaryKeyRelatedField anzeigen. Dies mag für einige Projekte akzeptabel sein, aber wenn Sie Ihr API-Schema mit Redoc oder Swagger anzeigen möchten, kann dies ein Problem sein. Deshalb bevorzuge ich eine Zwei-Feld-Lösung, auch wenn sie nicht einfach und schön ist.
Youngrok
5
Ich bin überrascht, dass DRF diese Arbeit braucht, um dies zu tun. Wie die meisten Leute hier wäre der typische Anwendungsfall, ForeignKey-Objekte als Daten zurückzugeben, sie aber als PKs zu akzeptieren. Ich denke, dies ist tatsächlich ein häufigerer Anwendungsfall als das Erstellen oder ausschließliche Lesen von ForeignKeys ...
Kevin Parker
@JPG Bei Verwendung dieser Methode wird beim Ausführen eines Patch-Updates der folgende Fehler angezeigt. Falscher Typ. Erwarteter pk-Wert, empfangenes <Modell>. Das Feld ist im verschachtelten Serializer vorhanden. Was ist hier schief gelaufen?
AKHIL MATHEW
Möglicherweise ist ein verschachtelter Serializer oder ähnliches definiert . Ich bin mir ziemlich sicher, dass dieses Beispiel ein funktionierendes ist @AKHILMATHEW
JPG
52

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

In Ihren 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

Die Einstellung source=childlässt sich child_idstandardmäßig wie ein Kind verhalten, wenn sie nicht überschrieben wird (unser gewünschtes Verhalten). write_only=Truestellt child_iddas Schreiben zur Verfügung, verhindert jedoch, dass es in der Antwort angezeigt wird, da die ID bereits in der Antwort angezeigt wird ChildSerializer.

Joslarson
quelle
4
Ich habe die folgende Fehlermeldung erhalten:Got a TypeError when calling Parent.objects.create(). This may be because you have a writable field on the serializer class that is not a valid argument to Parent.objects.create(). You may need to make the field read-only, or override the ParentSerializer.create() method to handle this correctly.
Gobi Dasu
4

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
Anton Dmitrievsky
quelle
Wenn Sie DRF 3.0 verwenden, ist dies eine gute Lösung. Beachten Sie jedoch, dass das nach dem Erstellen von Parent zurückgegebene Parent-Element keine verschachtelte Child-Serialisierung aufweist, sondern flach ist (nur der Primärschlüssel). Um dies zu beheben, müssen Sie auch die Methode to_representation überschreiben. Ich füge dies in meiner Antwort auf eine doppelte Frage hinzu: stackoverflow.com/questions/26561640/…
jeffjv
Vielen Dank! Ich habe meinen Tag damit verschwendet, dieses Problem zu lösen ... Die ausgewählte Antwort funktioniert bei mir nicht ...
Gutimore
4

Einige Leute hier haben eine Möglichkeit gefunden, ein Feld zu behalten, können aber dennoch die Details beim Abrufen des Objekts abrufen und es nur mit der ID erstellen. Ich habe eine etwas allgemeinere Implementierung vorgenommen, wenn die Leute interessiert sind:

Zunächst einmal 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 wie folgt, 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 mit der PK erstellen, aber beim Abrufen des von Ihnen erstellten Objekts (oder wann immer wirklich) das vollständig serialisierte verschachtelte Modell zurückgeben.

Bono
quelle
1
Sollte in DRF enthalten sein. :)
Igor Pomaranskiy
2

Ich denke, der von Kevin skizzierte Ansatz wäre wahrscheinlich die beste Lösung, aber ich konnte ihn nie zum Laufen bringen. DRF warf immer wieder Fehler, wenn ich sowohl einen verschachtelten Serializer als auch ein Primärschlüsselfeld festgelegt hatte. Das Entfernen des einen oder anderen würde funktionieren, brachte mir aber offensichtlich nicht das Ergebnis, das ich brauchte. Das Beste, was ich mir einfallen lassen kann, ist, zwei verschiedene Serializer zum Lesen und Schreiben zu erstellen.

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
Jay Nielsen
quelle
Ich habe das gleiche Problem wie Sie. Haben Sie jemals eine Möglichkeit gefunden, es in einem Serializer funktionsfähig zu machen? stackoverflow.com/questions/41248271
shanemgrey
2

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 Ihren verschachtelten untergeordneten Serializer nur so, wie Sie ihn vom Serializer erhalten, dh untergeordnet als JSON / Wörterbuch. In to_internal_valueinstanziieren wir das untergeordnete Objekt, wenn es eine gültige ID hat, damit DRF weiter mit dem Objekt arbeiten kann.

Gaurav Butola
quelle
0

Ich bin auch in der gleichen Situation festgefahren. Aber was ich getan habe, dass ich zwei Serializer für die folgenden Modelle wie folgt erstellt habe:

class Base_Location(models.Model):
    Base_Location_id = models.AutoField(primary_key = True)
    Base_Location_Name = models.CharField(max_length=50, db_column="Base_Location_Name")

class Location(models.Model):
    Location_id = models.AutoField(primary_key = True)
    Location_Name = models.CharField(max_length=50, db_column="Location_Name")
    Base_Location_id = models.ForeignKey(Base_Location, db_column="Base_Location_id", related_name="Location_Base_Location", on_delete=models.CASCADE)

Dies ist mein übergeordneter Serializer

class BaseLocationSerializer(serializers.ModelSerializer):
    class Meta:
        model = Base_Location
        fields = "__all__"

Ich benutze diesen Serializer nur, um eine Anfrage zu erhalten. Als Antwort habe ich Daten mit einem Fremdschlüssel erhalten, auch wegen des verschachtelten Serializers

class LocationSerializerList(serializers.ModelSerializer): <-- using for get request 
    Base_Location_id = BaseLocationSerializer() 

    class Meta:
        model = Location
        fields = "__all__"

Screenshot von Get Method Request und Response in Postman

Ich verwende diesen Serializer nur für Post-Anfragen. Beim Senden von Post-Anfragen muss ich also keine zusätzlichen Informationen anstelle des Primärschlüsselfeldwerts angeben

class LocationSerializerInsert(serializers.ModelSerializer): <-- using for post request
    class Meta:
        model = Location
        fields = "__all__"

Screenshot der Anforderung und Antwort der Postmethode beim Postboten

Zaid Pathan
quelle
0

Basierend auf den Antworten von JPG und Bono habe ich eine Lösung gefunden, die auch den OpenAPI-Schema-Generator von DRF unterstützt.

Die eigentliche Feldklasse ist:

from rest_framework import serializers


class ModelRepresentationPrimaryKeyRelatedField(serializers.PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.response_serializer_class = kwargs.pop('response_serializer_class', None)
        if self.response_serializer_class is not None \
                and not issubclass(self.response_serializer_class, serializers.Serializer):
            raise TypeError('"serializer" is not a valid serializer class')

        super(ModelRepresentationPrimaryKeyRelatedField, self).__init__(**kwargs)

    def use_pk_only_optimization(self):
        return False if self.response_serializer_class else True

    def to_representation(self, instance):
        if self.response_serializer_class is not None:
            return self.response_serializer_class(instance, context=self.context).data
        return super(ModelRepresentationPrimaryKeyRelatedField, self).to_representation(instance)

Die erweiterte AutoSchema-Klasse lautet:

import inspect
from rest_framework.schemas.openapi import AutoSchema

from .fields import ModelRepresentationPrimaryKeyRelatedField


class CustomSchema(AutoSchema):
    def _map_field(self, field):
        if isinstance(field, ModelRepresentationPrimaryKeyRelatedField) \
                and hasattr(field, 'response_serializer_class'):
            frame = inspect.currentframe().f_back
            while frame is not None:
                method_name = frame.f_code.co_name
                if method_name == '_get_request_body':
                    break
                elif method_name == '_get_responses':
                    field = field.response_serializer_class()
                    return super(CustomSchema, self)._map_field(field)

                frame = frame.f_back

        return super(CustomSchema, self)._map_field(field)

Anschließend können Sie in den Projekteinstellungen Ihres Dganjo diese neue Schema-Klasse definieren, die global verwendet werden soll:

REST_FRAMEWORK = {
    'DEFAULT_SCHEMA_CLASS': '<path_to_custom_schema>.CustomSchema',
}

Zuletzt können Sie innerhalb Ihrer Modelle den neuen Feldtyp wie folgt verwenden:

class ExampleSerializer(serializers.ModelSerializer):
    test_field = ModelRepresentationPrimaryKeyRelatedField(queryset=Test.objects.all(), response_serializer_class=TestListSerializer)
ziogaschr
quelle
0

Ich habe zunächst etwas Ähnliches wie die JPG-Lösung implementiert, bevor ich diese Antwort gefunden habe, und festgestellt, dass dadurch die Vorlagen des integrierten Django Rest Framework beschädigt werden. Nun, das ist keine so große Sache (da ihre Lösung wunderbar über Anfragen / Postbote / AJAX / Curl / etc. Funktioniert), aber wenn jemand neu ist (wie ich) und möchte, dass das eingebaute DRF-Formular ihm dabei hilft Hier ist meine Lösung (nachdem ich sie bereinigt und einige der Ideen von JPG integriert habe):

class NestedKeyField(serializers.PrimaryKeyRelatedField):
    def __init__(self, **kwargs):
        self.serializer = kwargs.pop('serializer', None)
        if self.serializer is not None and not issubclass(self.serializer, serializers.Serializer):
            raise TypeError('You need to pass a instance of serialzers.Serializer or atleast something that inherits from it.')
        super().__init__(**kwargs)

    def use_pk_only_optimization(self):
        return not self.serializer

    def to_representation(self, value):
        if self.serializer:
            return dict(self.serializer(value, context=self.context).data)
        else:
            return super().to_representation(value)

    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([
            (
                self.to_representation(item)['id'] if self.serializer else self.to_representation(item), # If you end up using another column-name for your primary key, you'll have to change this extraction-key here so it maps the select-element properly.
                self.display_value(item)
            )
            for item in queryset
        ])

und ein Beispiel unten, Child Serializer-Klasse:

class ChildSerializer(serializers.ModelSerializer):
    class Meta:
        model = ChildModel
        fields = '__all__'

Übergeordnete Serializer-Klasse:

class ParentSerializer(serializers.ModelSerializer):
    same_field_name_as_model_foreign_key = NestedKeyField(queryset=ChildModel.objects.all(), serializer=ChildSerializer)
    class Meta:
        model = ParentModel
        fields = '__all__'
Nickatak
quelle
0

Hier ist, was ich überall benutze. Dies ist möglicherweise die einfachste und einfachste Methode, bei der keine Hacks usw. erforderlich sind und bei der DRF direkt verwendet wird, ohne durch Reifen zu springen. Freut mich über Meinungsverschiedenheiten mit diesem Ansatz.

Rufen Sie in perform_create (oder einem gleichwertigen Element) der Ansicht das FK-Modelldatenbankobjekt ab, das dem in der POST-Anforderung gesendeten Feld entspricht, und senden Sie es dann an den Serializer. Das Feld in der POST-Anforderung kann alles sein, was zum Filtern und Lokalisieren des DB-Objekts verwendet werden kann. Es muss keine ID sein.

Dies ist hier dokumentiert: https://www.django-rest-framework.org/api-guide/generic-views/#genericapiview

Diese Hooks sind besonders nützlich, um Attribute festzulegen, die in der Anforderung enthalten sind, aber nicht Teil der Anforderungsdaten sind. Beispielsweise können Sie ein Attribut für das Objekt basierend auf dem Anforderungsbenutzer oder basierend auf einem URL-Schlüsselwortargument festlegen.

def perform_create (self, serializer): serializer.save (user = self.request.user)

Diese Methode hat auch den Vorteil, dass die Parität zwischen Lese- und Schreibseite erhalten bleibt, indem in der Antwort auf GET oder POST keine verschachtelte Darstellung für das Kind gesendet wird.

In Anbetracht des vom OP veröffentlichten Beispiels:

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):
    # Note this is different from the OP's example. This will send the
    # child name in the response
    child = serializers.ReadOnlyField(source='child.name')

    class Meta:
        model = Parent
        fields = ('name', 'phone_number', 'child')

In der Ansicht perform_create:

class SomethingView(generics.ListCreateAPIView):
    serializer_class = ParentSerializer
    
    def perform_create(self, serializer):
        child_name = self.request.data.get('child_name', None)
        child_obj = get_object_or_404(Child.objects, name=child_name)
        serializer.save(child=child_obj)

PS: Bitte beachten Sie, dass ich dieses obige Snippet nicht getestet habe, es jedoch auf einem Muster basiert, das ich an vielen Stellen verwende, sodass es so funktionieren sollte, wie es ist.

Zehawk
quelle