Django序列化器嵌套创建:如何避免对关系进行N + 1查询

时间:2018-11-24 23:16:15

标签: python django django-rest-framework django-serializer

在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

问题

我正在尝试使用以下示例数据来过帐到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个嵌套标签,我负担不起。
如何“预取”进行验证?
如果这是不可能的,我该如何关闭用于检查数据库中是否包含外键ID的验证?

编辑:附加信息

这是视图:

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 :(得分:1)

在我看来,DRF序列化器不是优化数据库查询的地方。序列化器有2个工作:

  1. 序列化并检查输入数据的有效性。
  2. 序列化输出数据。

因此,优化查询的正确位置是相应的视图。
我们将使用select_related方法:

  

返回一个查询集,该查询集将“遵循”外键关系,并在执行查询时选择其他相关对象数据。这可以提高性能,从而导致单个更复杂的查询,但意味着以后使用外键关系将不需要数据库查询。   以避免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.

如果您要测试还使用串行器减少查询数量的其他内容,则可以选中this article

答案 1 :(得分:0)

我认为这里的问题是Tag构造函数通过从数据库中查找将category传递的类别ID自动转换为TagCategory实例。如果您知道所有类别ID均有效,则可以通过执行以下操作来避免这种情况:


    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

答案 2 :(得分:0)

我想出了一个可以使事情正常运行的答案(但我并不感到兴奋):像这样修改标签序列化器:

class TagSerializer(serializers.ModelSerializer):

    category_id = serializers.IntegerField()

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

这使我可以读/写category_id,而无需进行验证。添加category以排除在外确实意味着如果在实例上设置了category,则序列化程序将忽略它。

答案 3 :(得分:0)

问题是您没有为图书实例设置创建的标签,因此序列化程序尝试在返回时获取该标签。

您需要将其设置为列表形式的书:

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

TagSerializer 提供Meta.fields元组(很奇怪的是,该序列化程序不会引发错误,指出需要 fields 元组)

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

在这种情况下,不必预取tag.category,因为它只是id。

您需要为 GET 方法预取 Book.tags 。最简单的解决方案是为序列化程序创建静态方法,并在 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()

答案 4 :(得分:0)

{ events( where: { AND: [ { location: { name: "Test" } } { time: { startDate_lt: "2018-12-03T13:46:13.021Z" endDate_gt: "2018-12-03T13:46:13.021Z" } } { participantList_some: { participant: { firstName: "Lorem", lastName: "Ipsum" } } } ] } ) { participantList (where: { participant: { firstName: "Lorem", lastName: "Ipsum" } }) { participant { firstName lastName } } location { name } } } 函数将在第一时间检查ForeignKey。 实际上,这是关系数据库中的ForeignKey检查,您可以在数据库中使用select_related来关闭检查。