Django Serializer Nested Creation: как избежать N + 1 запросов на отношения - PullRequest
0 голосов
/ 25 ноября 2018

В Django есть десятки сообщений о n + 1 запросах во вложенных отношениях, но я не могу найти ответ на свой вопрос.Вот контекст:

Модели

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

class Tag(models.Model):
    book = models.ForeignKey('app.Book', on_delete=models.CASCADE, related_name='tags')
    category = models.ForeignKey('app.TagCategory', on_delete=models.PROTECT)
    page = models.PositiveIntegerField()

class TagCategory(models.Model):
    title = models.CharField(max_length=255)
    key = models.CharField(max_length=255)

В книге много тегов, каждый тег относится к категории тегов.

Сериализаторы

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        exclude = ['id', 'book']

class BookSerializer(serializers.ModelSerializer):
    tags = TagSerializer(many=True, required=False)

    class Meta:
        model = Book
        fields = ['title', 'tags']

    def create(self, validated_data):
        with transaction.atomic():
            tags = validated_data.pop('tags')
            book = Book.objects.create(**validated_data)
            Tag.objects.bulk_create([Tag(book=book, **tag) for tag in tags])
        return book

Проблема

Я пытаюсь отправить POST в BookViewSet со следующими данными примера:

{ 
  "title": "The Jungle Book"
  "tags": [
    { "page": 1, "category": 36 }, // plot intro
    { "page": 2, "category": 37 }, // character intro
    { "page": 4, "category": 37 }, // character intro
    // ... up to 1000 tags
  ]
}

Это все работает, однако во время публикации сериализатор выполняет вызов для каждого тега, чтобы проверить, является ли category_id действительным:

enter image description here

Поскольку в вызове может быть до 1000 вложенных тегов, я не могу себе этого позволить.
Как мне «выполнить предварительную выборку» для проверки?
Если это невозможно, как отключить проверку, которая проверяет наличие в базе данных идентификатора Foreign_key?

РЕДАКТИРОВАТЬ:Дополнительная информация

Вот вид:

class BookViewSet(views.APIView):

    queryset = Book.objects.all().select_related('tags', 'tags__category')
    permission_classes = [IsAdminUser]

    def post(self, request, format=None):
        serializer = BookSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Ответы [ 5 ]

0 голосов
/ 04 декабря 2018

select_related функция проверит ForeignKey в первый раз.На самом деле, это проверка ForeignKey в реляционной базе данных, и вы можете использовать SET FOREIGN_KEY_CHECKS=0; в базе данных, чтобы закрыть проверку.

0 голосов
/ 25 ноября 2018

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

Вам необходимо установить его для книги в виде списка:

def create(self, validated_data):
    with transaction.atomic():
        book = Book.objects.create(**validated_data)

        # Add None as a default and check that tags are provided
        # If you don't do that, serializer will raise error if request don't have 'tags'

        tags = validated_data.pop('tags', None)
        tags_to_create = []

        if tags:
            tags_to_create = [Tag(book=book, **tag) for tag in tags]
            Tag.objects.bulk_create(tags_to_create)

        # Here I set tags to the book instance
        setattr(book, 'tags', tags_to_create)

    return book

Предоставить кортеж Meta.fields для TagSerializer (странно, что этот сериализатор не выдает ошибку, говоря, что поля кортеж требуется)

class TagSerializer(serializers.ModelSerializer):
    class Meta:
        model = Tag
        fields = ('category', 'page',)

Предварительная выборкаtag.category в этом случае НЕ требуется, потому что это просто id.

Вам понадобится предварительная выборка Book.tags для GET метода.Самое простое решение - создать статический метод для сериализатора и использовать его в методе viewset get_queryset , например:

class BookSerializer(serializers.ModelSerializer):
    ...
    @staticmethod
    def setup_eager_loading(queryset): # It can be named any name you like
        queryset = queryset.prefetch_related('tags')

        return queryset

class BookViewSet(views.APIView):
    ...
    def get_queryset(self):
        self.queryset = BookSerializer.setup_eager_loading(self.queryset)
        # Every GET request will prefetch 'tags' for every book by default

        return super(BookViewSet, self).get_queryset()
0 голосов
/ 25 ноября 2018

Я думаю, что проблема здесь в том, что конструктор Tag автоматически преобразует идентификатор категории, который вы передаете как category, в экземпляр TagCategory, просматривая его из базы данных.Чтобы избежать этого, сделайте что-то вроде следующего, если вы знаете, что все идентификаторы категории действительны:


    def create(self, validated_data):
        with transaction.atomic():
            tags = validated_data.pop('tags')
            book = Book.objects.create(**validated_data)
            tag_instances = [ Tag(book_id=book.id, page=x['page'], category_id=x['category']) for x in tags ]
            Tag.objects.bulk_create(tag_instances)
        return book
0 голосов
/ 25 ноября 2018

Я получил ответ, который заставляет все работать (но я не в восторге): Модифицируйте Сериализатор тегов следующим образом:

class TagSerializer(serializers.ModelSerializer):

    category_id = serializers.IntegerField()

    class Meta:
        model = Tag
        exclude = ['id', 'book', 'category']

Это позволяет мне читать / писатьcategory_id без накладных расходов на проверки.Добавление category для исключения означает, что сериализатор будет игнорировать category, если он установлен в экземпляре.

0 голосов
/ 25 ноября 2018

Сериализатор DRF не место (по моему мнению) для оптимизации запросов к БД.Сериализатор имеет 2 задания:

  1. Сериализация и проверка достоверности входных данных.
  2. Сериализация выходных данных.

Поэтому правильное место для оптимизации вашего запросаявляется соответствующим представлением.
Мы будем использовать метод select_related, который:

Возвращает QuerySet, который будет «следовать» отношениям внешнего ключа, выбирая дополнительные связанныеданные объекта, когда он выполняет свой запрос.Это повышение производительности, которое приводит к одному более сложному запросу, но означает, что дальнейшее использование отношений внешнего ключа не потребует запросов к базе данных.чтобы избежать запросов к базе данных N + 1.

Вам нужно будет изменить часть кода вашего представления, которая создает соответствующий набор запросов, чтобы включить вызов select_related.
Вы будететакже необходимо добавить related_name в определение поля Tag.category.

Пример :

# In your Tag model:
category = models.ForeignKey(
    'app.TagCategory', on_delete=models.PROTECT, related_name='categories'
)

# In your queryset defining part of your View:
class BookViewSet(views.APIView):

    queryset = Book.objects.all().select_related(
        'tags', 'tags__categories'
    )  # We are using the related_name of the ForeignKey relationships.

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

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