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 create
und update
Methoden, 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
quelle
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 derParentSerializer
der 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
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
parent
Instanz{ "name": "TestPOSTMAN_name", "phone_number": 1, "child": 1 }
Bildschirmfoto
quelle
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=child
lässt sichchild_id
standardmäßig wie ein Kind verhalten, wenn sie nicht überschrieben wird (unser gewünschtes Verhalten).write_only=True
stelltchild_id
das Schreiben zur Verfügung, verhindert jedoch, dass es in der Antwort angezeigt wird, da die ID bereits in der Antwort angezeigt wirdChildSerializer
.quelle
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.
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
quelle
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.
quelle
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
quelle
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_value
instanziieren wir das untergeordnete Objekt, wenn es eine gültige ID hat, damit DRF weiter mit dem Objekt arbeiten kann.quelle
Dafür gibt es ein Paket! Schauen Sie sich PresentablePrimaryKeyRelatedField im Drf Extra Fields-Paket an.
https://github.com/Hipo/drf-extra-fields
quelle
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)
class BaseLocationSerializer(serializers.ModelSerializer): class Meta: model = Base_Location fields = "__all__"
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
class LocationSerializerInsert(serializers.ModelSerializer): <-- using for post request class Meta: model = Location fields = "__all__"
Screenshot der Anforderung und Antwort der Postmethode beim Postboten
quelle
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)
quelle
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__'
quelle
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 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.
quelle