django prefetch_related应该与GenericRelation一起使用

Tod*_*dor 32 python django django-models django-orm

更新:关于此问题的Open Open:24272

什么都有关系?

Django有一个GenericRelation类,它增加了一个"反向"泛型关系来启用一个额外的API.

事实证明我们可以使用它reverse-generic-relation,filtering或者ordering我们不能在里面使用它prefetch_related.

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

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

让我们说我们有两个主要模型:MoviesBooks.

  • Movies 有一个 Director
  • Books 有一个 Author

我们要的标签分配给我们的MoviesBooks,但是,而是采用MovieTagBookTag模型,我们想用一个单一的TaggedItem与类GFKMovieBook.

这是模型结构:

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

还有一些初步数据:

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

因此,文档显示我们可以做这样的事情.

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

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

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

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

1)prefetchquerysets包含不同类型的一个深度content_object.

>>> TaggedItem.objects.all().prefetch_related('content_object')
[<TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: action movie>]
Run Code Online (Sandbox Code Playgroud)

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

但是,如果我们想要1)和2)(prefetchqueryset包含不同类型的许多级别content_objects,我们不能使用content_object.

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

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

也许用自定义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.
Run Code Online (Sandbox Code Playgroud)

此处显示了此问题的一些解决方案.但这是对我要避免的数据的大量按摩.我非常喜欢来自的API reversed generic relations,能够做到prefetchs这一点非常好:

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

或者像那样:

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

但正如你所看到的,我们离开了那个AttributeError.我正在使用Django 1.7.3和Python 2.7.6.我很好奇为什么Django会抛出这个错误?为什么Django的寻找一种object_idBook模型? 为什么我认为这可能是一个错误? 通常当我们要求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()
Run Code Online (Sandbox Code Playgroud)

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

Ber*_*ant 29

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

您还可以查看Django源代码中的相关测试.

此外,Django文档声明prefetch_related()应该使用GenericForeignKeyGenericRelation:

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

UPDATE:要预取content_objectTaggedItem,你可以使用TaggedItem.objects.all().prefetch_related('content_object'),如果你想限制结果只标记Book,你可以额外筛选的对象ContentType(如果没有把握prefetch_related作品用related_query_name).如果您还希望Author与本书一起使用select_related()而不是prefetch_related()因为这是一种ForeignKey关系,您可以在自定义prefetch_related()查询中将其组合:

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

  • Bernhard的最新更新代码应该起作用还是尝试解决问题?我在通用外键上尝试了它,它会抛出一个错误.查看django(contrib.contenttypes.fields.get_prefetch_queryset)的源代码,不允许为genericforeign密钥预取提供查询集. (7认同)
  • @eugene确切地说:`自定义查询集不能用于此查找 (4认同)

Tod*_*dor 6

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

  • 老实说,我不记得了,但是,现在做了一些测试并用一个工作示例更新了答案。不幸的是,`Prefetch` 对象中的自定义查询集似乎无法与 `GenericForeignKey` 一起使用,因此我们无法在 Book/Movie 查询集上执行 `select_related` 来获取作者/导演。 (2认同)