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
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 .
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
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
}
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
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.
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).
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
Dafür gibt es ein Paket! Schauen Sie sich PresentablePrimaryKeyRelatedField im Drf Extra Fields-Paket an.