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

zqdjd7g9  于 2023-04-22  发布在  Go
关注(0)|答案(6)|浏览(202)

关于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
  ]
}

这一切都可以工作,但是,在post期间,序列化器继续为每个标记进行调用,以检查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)
fsi0uk1n

fsi0uk1n1#

DRF序列化器不是优化DB查询的地方(在我自己看来)。序列化器有2个工作:
1.序列化并检查输入数据的有效性。
1.序列化输出数据。
因此,优化查询的正确位置是相应的视图。
我们将使用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

vc9ivgsu

vc9ivgsu2#

我认为这里的问题是Tag构造函数会通过从数据库中查找来自动将传入的类别id转换为category示例。避免这种情况的方法是,如果您知道所有类别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
hlswsv35

hlswsv353#

我想出了一个让事情运转起来的答案(但我并不感到兴奋):像这样修改标记序列化器:

class TagSerializer(serializers.ModelSerializer):

    category_id = serializers.IntegerField()

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

这允许我读/写category_id而不需要验证的开销。添加category到exclude确实意味着如果在示例上设置了category,序列化器将忽略它。

wsxa1bj1

wsxa1bj14#

问题是,您没有将创建的标记设置到book示例,因此序列化程序在返回时会尝试获取此标记。
您需要将其设置为图书列表:

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 tuple(奇怪的是,这个序列化器没有提示需要fieldstuple)

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

p8ekf7hl5#

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

mzsu5hc0

mzsu5hc06#

我知道这个问题已经存在了很长一段时间,但我有同样的问题,我找了几天的解决方案,最后我找到了另一个解决方案,为我工作。
我把它留在这里,以防它对某些人有帮助,这样它就不再对每个关系进行查询,现在它只是对所有关系的查询,在to_internal_value中它验证外键。

class TagSerializer(serializers.ModelSerializer):
    ...
    category_id = serializers.PrimaryKeyRelatedField(queryset = Category.objects.all(), source='category', write_only=True)
    ...

    def __init__(self, *args, **kwargs):
        self.categories = Category.objects.all().values_list('id', flat=True)
        super().__init__(*args, **kwargs)

    def to_internal_value(self, data):
        category_id = data.pop('category_id', None)

        if category_id is not None:
            if not category_id in self.categories:
                raise serializers.ValidationError({
                    'category_id': 'Category does not exist'
                })
        return super().to_internal_value(data)

相关问题