关于 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个嵌套标签,我受不起。
我如何“预取”验证?
如果这是不可能的,我该如何关闭检查数据库中是否存在外键 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)
DRF 序列化器不是优化数据库查询的地方(在我看来)。序列化程序有 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.
如果你想测试一些不同的东西,也使用序列化器来减少查询的数量,你可以查看这篇文章。
我认为这里的问题是
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
我想出了一个可以让事情正常运行的答案(但我对此并不感到兴奋):像这样修改标签序列化器:
class TagSerializer(serializers.ModelSerializer):
category_id = serializers.IntegerField()
class Meta:
model = Tag
exclude = ['id', 'book', 'category']
这使我可以读/写 category_id 而无需验证开销。添加
category
排除确实意味着序列化程序将忽略 category
如果它是在实例上设置的。
问题是您没有将创建的标签设置为书籍实例,因此序列化程序在返回时尝试获取它。
需要以列表的形式设置到书中:
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()
select_related
函数将在第一时间检查 ForeignKey。
实际上,这是关系数据库中的ForeignKey检查,您可以在数据库中使用SET FOREIGN_KEY_CHECKS=0;
关闭检查。
我知道这个问题已经存在很长时间了,但我遇到了同样的问题,我一直在寻找解决方案好几天,最后我找到了另一个对我有用的解决方案。
我把它留在这里以防它帮助某人,这样它就不再查询每个关系,现在它只是查询所有并且在
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)