Django Serializer Nested Creation:如何避免对关系进行 N+1 查询

jbo*_*ily 7 python django django-serializer django-rest-framework

有几十篇关于 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)
Run Code Online (Sandbox Code Playgroud)

一本书有很多标签,每个标签属于一个标签类别。

序列化器

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
Run Code Online (Sandbox Code Playgroud)

问题

我正在尝试BookViewSet使用以下示例数据POST :

{ 
  "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
  ]
}
Run Code Online (Sandbox Code Playgroud)

这一切都有效,但是,在发布期间,序列化程序继续调用每个标签以检查它是否category_id有效:

在此处输入图片说明

一次调用中多达 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)
Run Code Online (Sandbox Code Playgroud)

Joh*_*fis 6

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

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

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

\n\n
\n

返回一个 QuerySet,它将 \xe2\x80\x9cfollow\xe2\x80\x9d 外键关系,在执行查询时选择其他相关对象数据。这是一个性能增强器,会导致单个更复杂的查询,但意味着稍后使用外键关系将\xe2\x80\x99 不需要数据库查询。\n 以避免 N+1 数据库查询。

\n
\n\n

您将需要修改视图代码中创建相应查询集的部分,以便包含调用select_related
\n您还需要添加related_name一个Tag.category字段定义中。

\n\n

例子

\n\n
# In your Tag model:\ncategory = models.ForeignKey(\n    \'app.TagCategory\', on_delete=models.PROTECT, related_name=\'categories\'\n)\n\n# In your queryset defining part of your View:\nclass BookViewSet(views.APIView):\n\n    queryset = Book.objects.all().select_related(\n        \'tags\', \'tags__categories\'\n    )  # We are using the related_name of the ForeignKey relationships.\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

如果您想测试一些不同的东西,也使用序列化器来减少查询数量,您可以查看这篇文章

\n

  • 这应该通过 select_lated 完成,而不是预取。(因为它是一对多而不是多对多)。 (2认同)
  • @yrekkehs 你是绝对正确的,这就是为什么人们不应该尝试回答凌晨 2 点的原因:P。感谢您的捕获! (2认同)
  • @jbodily 由于 yrekkehs 发现了我的错误,您应该使用的正确方法是“select_lated”。我编辑了我的答案,你应该像现在一样尝试。带来不便敬请谅解! (2认同)