python - django prefetch_related 是否应该与 GenericRelation 一起使用

标签 python django django-models django-orm

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

到底是怎么回事?

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

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

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

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

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

  • 电影有一个导演
  • 书籍有一个作者

我们想为我们的 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>]

但是,如果我们尝试通过这个reverse generic relationshipprefetch一些TaggedItem相关数据,我们将会得到一个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)(从包含不同类型 content_objectsquerysetprefetch 多个级别,我们可以'不使用 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 报告过任何事情,所以这就是我首先在这里询问的原因。我无法追踪错误并自行决定这是错误还是可以实现的功能。

最佳答案

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

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

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

prefetch_related, on the other hand, does a separate lookup for each relationship, and does the ‘joining’ in Python. This allows it to prefetch many-to-many and many-to-one objects, which cannot be done using select_related, in addition to the foreign key and one-to-one relationships that are supported by select_related. It also supports prefetching of GenericRelation and GenericForeignKey.

更新:要为 TaggedItem 预取 content_object,您可以使用 TaggedItem.objects.all().prefetch_related(' content_object'),如果您想将结果限制为仅标记的 Book 对象,您可以另外过滤 ContentType(不确定是否 prefetch_relatedrelated_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')
    )
)

关于python - django prefetch_related 是否应该与 GenericRelation 一起使用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/28127135/

相关文章:

python - Django shell_plus : How to access Jupyter notebook in Docker Container

python - 从 Django 1.8 获取 TemplateDoesNotExist

django - 如何在 Django 中包含 urlpatterns?

python - 根据状态 :Available using Python? 过滤或查询 AWS RDS DBInstances 的正确方法是什么

python - discord.py 中的 client.wait_for 中的“检查未定义”

c++ - Numpy 的 __array_interface__ 不返回字典

Django 不会在 Postgres 上创建 ImageField

python - 导入 .txt 文件,其中包含逗号且数字之间没有空格

python - Django 鹡鸰 'expected string or bytes-like object' 错误

python - Django Order By 对反向外键值删除没有关系的项目