Подтвердить что ты не робот

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
4b9b3361

Ответ 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, но вернет полную сериализованную вложенную модель при извлечении созданного вами объекта (или когда-либо действительно).