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

11 浏览
0 Comments

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

2022年更新:我在8年前提出的原始#24272号问题已经关闭,支持#33651号问题,一旦实现,将为我们提供一种新的语法来进行此类预取。

============== 更新结束 ==============

这是什么意思?

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

事实证明,我们可以使用这个“反向泛型关系”进行过滤或排序,但不能在prefetch_related中使用它。

我想知道这是一个bug,还是不应该工作,或者是可以在将来实现的东西。

让我用一些例子来说明我是什么意思。

假设我们有两个主要模型:电影和书籍。

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

我们想要给我们的电影和书籍分配标签,但是我们不想使用MovieTag和BookTag模型,而是想使用一个单独的TaggedItem类,并使用GFK将其与Movie或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')

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

>>> b1.tags.all()
[]
>>> m1.tags.all()
[]
>>> TaggedItem.objects.filter(books__author__name='E L James')
[, , ]
>>> TaggedItem.objects.filter(movies__director__name='James Gunn')
[]
>>> Book.objects.all().prefetch_related('tags')
[, , ]
>>> Book.objects.filter(tags__tag='roman')
[, , ]

但是,如果我们尝试通过这个反向泛型关系的预取来预取一些相关数据,我们将会得到一个AttributeError。

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

你们中的一些人可能会问,为什么我不使用content_object而不是books在这里?原因是,因为这只适用于以下情况:

  1. 只从包含不同类型content_object的查询集中预取一级。

    >>> TaggedItem.objects.all().prefetch_related('content_object')
    [, , , ]
    

  2. 从只包含一种类型content_object的查询集预取多级。

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

但是,如果我们既要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。

现在想象一下这样的情况,我们想要预取不仅带有他们的的书籍,还有带有他们的导演的电影。下面是一些尝试。

愚蠢的方式:

>>> 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,如果能够像这样进行预取,那将非常好:

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

为什么我认为这可能是一个bug?

通常,当我们要求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实际上试图解析关系...并失败了。这是一个应该报告的bug吗?我从来没有向Django报告过任何问题,所以我在这里先问问。我无法追踪这个错误并自行决定这是否是一个bug,或者是一个可以实现的功能。

0