django prefetch_related应该与GenericRelation一起使用

时间:2015-01-24 15:38:39

标签: python django django-models django-orm

更新:此问题已打开:24272

关于什么?

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

事实证明,我们可以将此reverse-generic-relation用于filteringordering,但我们无法在prefetch_related内使用它。

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

让我用一些例子向你展示我的意思。

让我们说我们有两个主要模型: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

一些初始数据:

>>> 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>]

但是,如果我们通过此prefetch尝试related data TaggedItem reverse generic relation >>> TaggedItem.objects.all().prefetch_related('books') Traceback (most recent call last): ... AttributeError: 'Book' object has no attribute 'object_id' ,我们将获得 AttributeError 。< / p>

content_object

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

1)querysets只有content_object的一个级别,包含不同类型的>>> TaggedItem.objects.all().prefetch_related('content_object') [<TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: action movie>]

prefetch

2)querysets多个级别,但content_object只包含一种>>> TaggedItem.objects.filter(books__author__name='E L James').prefetch_related('content_object__author') [<TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: roman>]

prefetch

但是,如果我们想要包含不同类型queryset的{​​{1}}的{​​1}}中的1)和2)(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'

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

现在想象一下我们prefetch books不仅authormovies,还有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. 对象?

reversed generic relations

显示了此问题的一些解决方案here。但是,对于我想要避免的数据,我需要进行大量的按摩。 我非常喜欢来自prefetchs的API,能够>>> 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'

或者那样:

1.7.3

但正如您所看到的,我们远离 AttributeError 。 我使用Django 2.7.6和Python object_id。我很好奇为什么Django会抛出这个错误?为什么Django在Book模型中搜索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() 解决某些问题时,我们会看到:

{{1}}

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

2 个答案:

答案 0 :(得分:28)

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

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

Django documentation表示prefetch_related()应与GenericForeignKeyGenericRelation合作:

  另一方面,

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

更新:如果要将结果限制为仅标记content_object,要为TaggedItem预取TaggedItem.objects.all().prefetch_related('content_object'),您可以使用Book您可以另外过滤ContentType的对象(不确定prefetch_related是否适用于related_query_name)。如果您还希望将Author与书籍一起使用,则需要使用select_related()而不是prefetch_related(),因为这是ForeignKey关系,您可以将其合并到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') ) ) 3}}:

{{1}}

答案 1 :(得分:1)

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