django prefetch_related 是不是应该与 GenericRelation 一起使用

Posted

技术标签:

【中文标题】django prefetch_related 是不是应该与 GenericRelation 一起使用【英文标题】:Is django prefetch_related supposed to work with GenericRelationdjango prefetch_related 是否应该与 GenericRelation 一起使用 【发布时间】:2015-03-23 12:13:16 【问题描述】:

更新:关于此问题的公开标记:24272

到底是怎么回事?

Django 有一个GenericRelation 类,它添加了一个“反向”通用关系以启用额外的API

事实证明,我们可以将这个reverse-generic-relation 用于filteringordering,但我们不能在prefetch_related 中使用它。

我想知道这是否是一个错误,或者它不应该工作,或者它可以在该功能中实现。

让我用一些例子告诉你我的意思。

假设我们有两个主要模型:MoviesBooks

Movies 有一个Director Books 有一个 Author

我们想给我们的MoviesBooks 分配标签,但是我们不想使用MovieTagBookTag 模型,而是使用一个TaggedItem 类和GFK 到@987654343 @ 或Book

这是模型结构:

from django.db import models
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType


class TaggedItem(models.Model):
    tag = models.SlugField()
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

    def __unicode__(self):
        return self.tag


class Director(models.Model):
    name = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name


class Movie(models.Model):
    name = models.CharField(max_length=100)
    director = models.ForeignKey(Director)
    tags = GenericRelation(TaggedItem, related_query_name='movies')

    def __unicode__(self):
        return self.name


class Author(models.Model):
    name = models.CharField(max_length=100)

    def __unicode__(self):
        return self.name


class Book(models.Model):
    name = models.CharField(max_length=100)
    author = models.ForeignKey(Author)
    tags = GenericRelation(TaggedItem, related_query_name='books')

    def __unicode__(self):
        return self.name

还有一些初始数据:

>>> from tags.models import Book, Movie, Author, Director, TaggedItem
>>> a = Author.objects.create(name='E L James')
>>> b1 = Book.objects.create(name='Fifty Shades of Grey', author=a)
>>> b2 = Book.objects.create(name='Fifty Shades Darker', author=a)
>>> b3 = Book.objects.create(name='Fifty Shades Freed', author=a)
>>> d = Director.objects.create(name='James Gunn')
>>> m1 = Movie.objects.create(name='Guardians of the Galaxy', director=d)
>>> t1 = TaggedItem.objects.create(content_object=b1, tag='roman')
>>> t2 = TaggedItem.objects.create(content_object=b2, tag='roman')
>>> t3 = TaggedItem.objects.create(content_object=b3, tag='roman')
>>> t4 = TaggedItem.objects.create(content_object=m1, tag='action movie')

所以正如docs 的节目,我们可以做这样的事情。

>>> b1.tags.all()
[<TaggedItem: roman>]
>>> m1.tags.all()
[<TaggedItem: action movie>]
>>> TaggedItem.objects.filter(books__author__name='E L James')
[<TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: roman>]
>>> TaggedItem.objects.filter(movies__director__name='James Gunn')
[<TaggedItem: action movie>]
>>> Book.objects.all().prefetch_related('tags')
[<Book: Fifty Shades of Grey>, <Book: Fifty Shades Darker>, <Book: Fifty Shades Freed>]
>>> Book.objects.filter(tags__tag='roman')
[<Book: Fifty Shades of Grey>, <Book: Fifty Shades Darker>, <Book: Fifty Shades Freed>]

但是,如果我们尝试通过这个reverse generic relation prefetchTaggedItem 中的一些related data,我们将得到一个AttributeError

>>> TaggedItem.objects.all().prefetch_related('books')
Traceback (most recent call last):
  ...
AttributeError: 'Book' object has no attribute 'object_id'

有些人可能会问,为什么我在这里不使用content_object 而不是books?原因是,因为这只在我们想要的时候才有效:

1) prefetch 仅比 querysets 深一层,包含不同类型的 content_object

>>> TaggedItem.objects.all().prefetch_related('content_object')
[<TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: action movie>]

2) prefetch 很多级别,但来自 querysets 仅包含一种类型的 content_object

>>> TaggedItem.objects.filter(books__author__name='E L James').prefetch_related('content_object__author')
[<TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: roman>]

但是,如果我们想要 1) 和 2)(从 queryset 到包含不同类型 content_objects 的多个级别的 prefetch,我们不能使用 content_object

>>> TaggedItem.objects.all().prefetch_related('content_object__author')
Traceback (most recent call last):
  ...
AttributeError: 'Movie' object has no attribute 'author_id'

Django 认为所有content_objects 都是Books,因此他们有一个Author

现在想象一下我们想要prefetch 不仅是books 和他们的author,还有movies 和他们的director。这里有一些尝试。

愚蠢的方式:

>>> TaggedItem.objects.all().prefetch_related(
...     'content_object__author',
...     'content_object__director',
... )
Traceback (most recent call last):
  ...
AttributeError: 'Movie' object has no attribute 'author_id'

也许使用自定义 Prefetch 对象?

>>>
>>> TaggedItem.objects.all().prefetch_related(
...     Prefetch('content_object', queryset=Book.objects.all().select_related('author')),
...     Prefetch('content_object', queryset=Movie.objects.all().select_related('director')),
... )
Traceback (most recent call last):
  ...
ValueError: Custom queryset can't be used for this lookup.

这个问题的一些解决方案显示在here。但这是对我想要避免的数据的大量按摩。 我真的很喜欢来自reversed generic relations 的API,能够像prefetchs 那样做会非常好:

>>> TaggedItem.objects.all().prefetch_related(
...     'books__author',
...     'movies__director',
... )
Traceback (most recent call last):
  ...
AttributeError: 'Book' object has no attribute 'object_id'

或者这样:

>>> TaggedItem.objects.all().prefetch_related(
...     Prefetch('books', queryset=Book.objects.all().select_related('author')),
...     Prefetch('movies', queryset=Movie.objects.all().select_related('director')),
... )
Traceback (most recent call last):
  ...
AttributeError: 'Book' object has no attribute 'object_id'

但是正如你所看到的,我们得到了 AttributeError。 我正在使用 Django 1.7.3 和 Python 2.7.6。我很好奇为什么 Django 会抛出这个错误?为什么 Django 在 Book 模型中搜索 object_id为什么我认为这可能是一个错误? 通常,当我们要求prefetch_related 解决它无法解决的问题时,我们会看到:

>>> TaggedItem.objects.all().prefetch_related('some_field')
Traceback (most recent call last):
  ...
AttributeError: Cannot find 'some_field' on TaggedItem object, 'some_field' is an invalid parameter to prefetch_related()

但在这里,情况有所不同。 Django 实际上试图解决这种关系......但失败了。这是一个应该报告的错误吗?我从来没有向 Django 报告过任何事情,所以这就是我首先在这里询问的原因。我无法追踪错误并自行决定这是错误还是可以实现的功能。

【问题讨论】:

好的,看看 Django Source 我会说这不是一个错误,但根本不支持......如果你想获得作者的书籍,你需要使用 select_related() 这样是ForeignKey 关系。要将其与 prefetch_related 一起使用,您需要使用自定义查询集,目前 Django 的 not supported 用于通用关系。 好的,谢谢。我opened a ticket 关于这个。希望有一天这个功能能够通过 ORM :) 【参考方案1】:

如果您想检索Book 实例并预取相关标签,请使用Book.objects.prefetch_related('tags')。这里不需要使用反向关系。

您也可以查看Django source code中的相关测试。

另外Django documentation 声明prefetch_related() 应该与GenericForeignKeyGenericRelation 一起使用:

另一方面,prefetch_related 对每个关系进行单独的查找,并在 Python 中进行“连接”。除了 select_related 支持的外键和一对一关系之外,这允许它预取多对多和多对一对象,这是使用 select_related 无法完成的。它还支持GenericRelationGenericForeignKey的预取。

更新:要为TaggedItem 预取content_object,您可以使用TaggedItem.objects.all().prefetch_related('content_object'),如果您想将结果限制为仅标记为Book 的对象,您可以另外过滤ContentType(不确定prefetch_related 是否与related_query_name 一起使用)。如果您还想将Author 与您需要使用的书一起获得select_related() 而不是prefetch_related(),因为这是ForeignKey 关系,您可以将其组合成custom prefetch_related() query:

from django.contrib.contenttypes.models import ContentType
from django.db.models import Prefetch

book_ct = ContentType.objects.get_for_model(Book)
TaggedItem.objects.filter(content_type=book_ct).prefetch_related(
    Prefetch(
        'content_object',  
        queryset=Book.objects.all().select_related('author')
    )
)

【讨论】:

感谢您的回复@Bernhard。问题是我真正需要的是prefetch books 和他们的author 来自TaggetItem queryset 包含 不仅books。这意味着我需要这样的东西:TaggedItem.objects.all().prefetch_related('books__author')。但这会引发一个奇怪的错误:AttributeError: 'Book' object has no attribute 'object_id'。我可以问问你对此的看法吗?在您看来,它是不是应该报告的错误? here 提供了此问题的一些解决方法。但这是我想要避免的大量数据自定义按摩。我喜欢来自reversed generic relations 的API,我相信这是实现此类功能的方式。 @Todor 我已经更新了我的答案,现在无法尝试,但我希望它能为您指明正确的方向......您可能想通过related_query_name 尝试一下以及对 content_type 的过滤... Bernhard 上次更新的代码应该可以工作还是试图解决问题?我在通用外键上尝试过它,它会引发错误。查看 django 的源代码(contrib.contenttypes.fields.get_prefetch_queryset),您不允许为通用外键预取提供查询集。 @eugene 确切地说:Custom queryset can't be used for this lookup.【参考方案2】:

prefetch_related_objects 来救援。

从 Django 1.10 开始(注意:它仍然存在于以前的版本中,但不是公共 API 的一部分。),我们可以使用prefetch_related_objects 来分治我们的问题。

prefetch_related 是一个操作,其中 Django 查询集被评估后获取相关数据(在评估主要查询集之后执行第二个查询)。为了工作,它期望查询集中的项目是同质的(相同的类型)。反向泛型生成现在不起作用的主要原因是我们有来自不同内容类型的对象,而代码还不够聪明,无法将不同内容类型的流程分开。

现在使用prefetch_related_objects,我们只在查询集的子集 上获取所有项目都是同质的。这是一个例子:

from django.db import models
from django.db.models.query import prefetch_related_objects
from django.core.paginator import Paginator
from django.contrib.contenttypes.models import ContentType
from tags.models import TaggedItem, Book, Movie


tagged_items = TaggedItem.objects.all()
paginator = Paginator(tagged_items, 25)
page = paginator.get_page(1)

# prefetch books with their author
# do this only for items where
# tagged_item.content_object is a Book
book_ct = ContentType.objects.get_for_model(Book)
tags_with_books = [item for item in page.object_list if item.content_type_id == book_ct.id]
prefetch_related_objects(tags_with_books, "content_object__author")

# prefetch movies with their director
# do this only for items where
# tagged_item.content_object is a Movie
movie_ct = ContentType.objects.get_for_model(Movie)
tags_with_movies = [item for item in page.object_list if item.content_type_id == movie_ct.id]
prefetch_related_objects(tags_with_movies, "content_object__director")

# This will make 5 queries in total
# 1 for page items
# 1 for books
# 1 for book authors
# 1 for movies
# 1 for movie directors
# Iterating over items wont make other queries
for item in page.object_list:
    # do something with item.content_object
    # and item.content_object.author/director
    print(
        item,
        item.content_object,
        getattr(item.content_object, 'author', None),
        getattr(item.content_object, 'director', None)
    )

【讨论】:

你试过运行你的代码吗?因为它会触发相同的ValueError: Custom queryset can't be used for this lookup.。而且你还有缺少括号的语法错误。 说实话,我不记得了,但是,现在做了一些测试并用一个工作示例更新了答案。不幸的是,Prefetch 对象中的自定义查询集似乎无法与 GenericForeignKey 一起使用,因此我们不能在 Book/Movie 查询集上执行 select_related 来获取作者/导演。

以上是关于django prefetch_related 是不是应该与 GenericRelation 一起使用的主要内容,如果未能解决你的问题,请参考以下文章

Django:prefetch_related 没有效果

Django prefetch_related 与限制

Django:4个表中的select_related() / prefetch_related()

prefetch_related 上的 Django ORM 注释

django prefetch_related 很多查询

使用 prefetch_related 优化 Django Queryset 多对多 for 循环