Django-rest-framework 3.0 создавать или обновлять в вложенном сериализаторе

С django-rest-framework 3.0 и имеющим эти простые модели:

class Book(models.Model):
    title = models.CharField(max_length=50)


class Page(models.Model):
    book = models.ForeignKey(Books, related_name='related_book')
    text = models.CharField(max_length=500)

И учитывая этот запрос JSON:

{
   "book_id":1,
   "pages":[
      {
         "page_id":2,
         "text":"loremipsum"
      },
      {
         "page_id":4,
         "text":"loremipsum"
      }
   ]
}

Как я могу написать вложенный сериализатор для обработки этого JSON и для каждого page для данного book либо создать новую страницу, либо обновить, если она существует.

class RequestSerializer(serializers.Serializer):
    book_id = serializers.IntegerField()
    page = PageSerializer(many=True)


class PageSerializer(serializers.ModelSerializer):
    class Meta:
        model = Page

Я знаю, что создание экземпляра сериализатора с помощью instance будет обновлять текущий, но как его использовать внутри метода create вложенного сериализатора?

Ответ 1

Во-первых, хотите ли вы поддерживать создание новых экземпляров книги или обновление только существующих?

Если вы только хотели создать новые экземпляры книг, вы могли бы сделать что-то вроде этого...

class PageSerializer(serializers.Serializer):
    text = serializers.CharField(max_length=500)

class BookSerializer(serializers.Serializer):
    page = PageSerializer(many=True)
    title = serializers.CharField(max_length=50)

    def create(self, validated_data):
        # Create the book instance
        book = Book.objects.create(title=validated_data['title'])

        # Create or update each page instance
        for item in validated_data['pages']:
            page = Page(id=item['page_id'], text=item['text'], book=book)
            page.save()

        return book

Обратите внимание, что здесь я не включил book_id. Когда мы создаем экземпляры книг, мы не будем включать идентификатор книги. Когда мы обновляем экземпляры книг, мы обычно включаем идентификатор книги как часть URL-адреса, а не в данные запроса.

Если вы хотите поддерживать как создание, так и обновление экземпляров книг, вам нужно подумать о том, как вы хотите обрабатывать страницы, которые не включены в запрос, но в настоящее время связаны с экземпляром книги.

Вы можете спокойно игнорировать эти страницы и оставить их такими, какие они есть, возможно, вы захотите поднять ошибку проверки, или вы можете удалить их.

Предположим, что вы хотите удалить любые страницы, не включенные в запрос.

def create(self, validated_data):
    # As before.
    ...

def update(self, instance, validated_data):
    # Update the book instance
    instance.title = validated_data['title']
    instance.save()

    # Delete any pages not included in the request
    page_ids = [item['page_id'] for item in validated_data['pages']]
    for page in instance.books:
        if page.id not in page_ids:
            page.delete()

    # Create or update page instances that are in the request
    for item in validated_data['pages']:
        page = Page(id=item['page_id'], text=item['text'], book=instance)
        page.save()

    return instance

Также возможно, что вы можете поддерживать только обновления книг, а не поддерживать создание, и в этом случае включать только метод update().

Существуют также различные способы сокращения числа запросов, например. используя объемное создание/удаление, но вышеуказанное выполнило бы работу довольно простым способом.

Как вы видите, есть тонкости в типах поведения, которые могут возникнуть при работе с вложенными данными, поэтому тщательно подумайте о том, какое поведение вы ожидаете в разных случаях.

Также обратите внимание, что я использовал Serializer в приведенном выше примере, а не ModelSerializer. В этом случае проще просто включить все поля в классе сериализатора явно, вместо того, чтобы полагаться на автоматический набор полей, которые ModelSerializer генерирует по умолчанию.