DRF: Простое назначение внешнего ключа с вложенными сериализаторами?
С помощью Django REST Framework стандартный ModelSerializer позволит назначать или изменять отношения модели ExternalKey POST-идентификатором в виде Integer.
Какой самый простой способ получить это поведение из вложенного сериализатора?
Заметьте, я говорю только о назначении существующих объектов базы данных, а не вложенном создании.
В прошлом я обманул это с дополнительными полями "id" в сериализаторе и с пользовательскими методами create
и update
, но это такая, казалось бы, простая и частая проблема для меня, что мне любопытно знать лучший способ.
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
Ответы
Ответ 1
Лучшее решение здесь - использовать два разных поля: одно для чтения, другое - для записи. Без особого подъема трудно получить то, что вы ищете в одном поле.
Поле только для чтения будет вашим вложенным сериализатором (ChildSerializer
в этом случае), и это позволит вам получить то же самое вложенное представление, которое вы ожидаете. Большинство людей определяют это как просто child
, потому что у них уже есть свой front-end, написанный этой точкой, и его изменение вызовет проблемы.
Поле с записью будет PrimaryKeyRelatedField
, что вы обычно использовали для назначения объектов на основе их первичного ключа. Это не должно быть только для записи, особенно если вы пытаетесь пойти на симметрию между тем, что получено и что отправлено, но похоже, что это может подойти вам лучше всего. В этом поле должно быть a source
установлено поле внешнего ключа (child
в этом примере), поэтому он правильно назначает его при создании и обновлении.
Это несколько раз поднималось на дискуссионную группу, и я думаю, что это по-прежнему лучшее решение. Спасибо Свену Мауреру за то, что он указал.
Ответ 2
Вот пример того, о чем говорит ответ кевина, если вы хотите использовать этот подход и использовать 2 отдельных поля.
В ваших 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)
тогда 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
Установка source=child
позволяет child_id
действовать как дочерний элемент по умолчанию, если бы он не был переопределен (наше желаемое поведение). write_only=True
делает child_id
доступным для записи, но предотвращает его отображение в ответе, поскольку идентификатор уже отображается в ChildSerializer
.
Ответ 3
Использование двух разных полей было бы нормально (как упоминали @Kevin Brown и @joslarson), но я думаю, что это не идеально (для меня). Потому что получение данных из одного ключа (child
) и отправка данных в другой ключ (child_id
) может быть немного неоднозначным для внешних разработчиков. (вообще без обид)
Поэтому я предлагаю переопределить метод to_representation()
метода ParentSerializer
.
def to_representation(self, instance):
response = super().to_representation(instance)
response['child'] = ChildSerializer(instance.child).data
return response
Полное представление Сериализатора
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
Преимущество этого метода?
Используя этот метод, нам не нужно два отдельных поля для создания и чтения. Здесь и создание, и чтение могут быть выполнены с помощью клавиши child
.
Пример полезной нагрузки для создания экземпляра parent
{
"name": "TestPOSTMAN_name",
"phone_number": 1,
"child": 1
}
Скриншот
![POSTMAN screenshot]()
Ответ 4
Есть способ заменить поле на операцию create/update:
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
Ответ 5
Я думаю, что подход, намеченный Кевином, вероятно, был бы лучшим решением, но я не мог заставить его работать. DRF продолжал бросать ошибки, когда у меня был как вложенный сериализатор, так и поле первичного ключа. Удаление того или другого будет функционировать, но, очевидно, не дало мне результата, который мне нужен. Лучшее, что я мог придумать, - создать два разных сериализатора для чтения и записи, например...
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
Ответ 6
Вот как я решил эту проблему.
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)
Вы просто передадите свой вложенный дочерний сериализатор так же, как вы получите его из сериализатора, то есть ребенка как json/dictionary. в to_internal_value
мы создаем экземпляр дочернего объекта, если он имеет действительный идентификатор, чтобы DRF мог продолжить работу с объектом.
Ответ 7
Несколько человек здесь нашли способ сохранить одно поле, но все же смогут получить детали при получении объекта и создать его только с идентификатором. Я сделал немного более общую реализацию, если люди заинтересованы:
Сначала тесты:
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)
Тогда сам класс:
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
Использование примерно так, если у вас есть сериализатор где-то:
class YourSerializer(ModelSerializer):
something_else = ModelRepresentationPrimaryKeyRelatedField(queryset=SomethingElse.objects.all(), model_serializer_class=SomethingElseSerializer)
Это позволит вам создать объект с внешним ключом, все еще только с PK, но вернет полную сериализованную вложенную модель при извлечении созданного вами объекта (или когда-либо действительно).
Ответ 8
Для этого есть пакет! Проверьте PresentablePrimaryKeyRelatedField в пакете Drf Extra Fields.
https://github.com/Hipo/drf-extra-fields