小编典典

Django prefetch_related应该与GenericRelation一起使用吗

python

更新:
有关此问题的公开答问24272

到底是什么

Django具有GenericRelation类,该类添加了
“反向”通用关系 以启用其他 API

事实证明,我们可以将其reverse-generic-relation用于filteringor
ordering,但是不能在内部使用它prefetch_related

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

让我通过一些示例向您展示我的意思。

可以说我们有两个主要模型:MoviesBooks

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

我们要分配代码发送给我们Movies,并Books而是,而是采用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')

因此,如文档所示,我们可以做这样的事情。

>>> 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 dataTaggedItem通过这个reverse generic relation,我们将得到一个 AttributeError的

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

你们中的有些人可能会问,为什么我不使用content_object而不是books这里?原因是,因为这仅在我们想要执行以下操作时起作用:

1)prefetchquerysets包含不同类型的到仅一个深度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)(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'

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.

这里显示了此问题的一些解决方案。但这对我想避免的数据有很多不足。我真的很喜欢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'

或者像这样:

>>> 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 。我正在使用Django1.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()

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


阅读 213

收藏
2021-01-20

共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)
    )
2021-01-20