【问题标题】:Django Serializer Nested Creation: How to avoid N+1 queries on relationsDjango Serializer 嵌套创建:如何避免对关系的 N+1 查询
【发布时间】:2019-04-27 01:00:15
【问题描述】:

在 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 是否有效:

一个调用中最多有 1000 个嵌套标签,我负担不起。
如何“预取”验证?
如果这是不可能的,我该如何关闭检查foreign_key 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)

【问题讨论】:

  • 使用.prefetch_related()函数解决

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


【解决方案1】:

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.

如果你想测试一些不同的东西,也使用序列化器来减少查询的数量,你可以检查this article

【讨论】:

  • 我已将视图添加到问题中。我按照您的建议做了,并添加了与视图相关的 prefetch_related 以获取标签和 tags__category(它是一对多的),但在创建时它仍然为 tag_category 每个标签查询一次。
  • 这应该通过 select_related 完成,而不是预取。 (因为它是一对多而不是多对多)。
  • @yrekkehs 你是绝对正确的,这就是为什么人们不应该尝试在早上回答 2 的原因:P。谢谢你的收获!
  • @jbodily 当 yrekkehs 发现我的错误时,您应该使用的正确方法是 select_related。我编辑了我的答案,你应该像现在这样尝试。给您带来的不便深表歉意!
  • 我很欣赏尽职调查,但不幸的是它似乎对创作没有帮助。让我再次澄清一下关系:一本书有很多标签,一个标签有一个book。一个tag 有一个tag_category,一个tag_category 有很多标签。所以两个一对多的关系。没有tags__categories。但其次,我更新了我的问题以反映您的解决方案,但仍然发现在创建时,每个类别都会查询一次数据库。
【解决方案2】:

我认为这里的问题是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

【讨论】:

  • 这感觉应该是对的,但请查看我添加的更新:在视图中我调用了serializer.is_valid(),然后调用了serializer.save()。问题在于 is_valid 检查,如果设置了 category_id 而不是类别,它会显示 category: This field may not be null.
【解决方案3】:

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

class TagSerializer(serializers.ModelSerializer):

    category_id = serializers.IntegerField()

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

这使我可以在没有验证开销的情况下读取/写入 category_id。将category 添加到exclude 确实意味着如果在实例上设置了category,则序列化程序将忽略它。

【讨论】:

    【解决方案4】:

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

    您需要将其设置为列表:

    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()
    

    【讨论】:

      【解决方案5】:

      select_related 函数会在第一时间检查 ForeignKey。 实际上,这是关系数据库中的 ForeignKey 检查,您可以使用数据库中的SET FOREIGN_KEY_CHECKS=0; 来关闭检查。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-02-16
        • 1970-01-01
        • 1970-01-01
        • 2015-03-26
        • 1970-01-01
        相关资源
        最近更新 更多