UPDATE:关于这个问题的公开信:24272

这是怎么回事?

Django有一个GenericRelation类,它增加了一个“reverse” generic relationship类来启用另一个API类.

事实证明,我们可以用reverse-generic-relation来表示filteringordering,但不能用prefetch_related来表示.

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

让我举几个例子告诉你我的意思.

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

  • Movies分有Director
  • Books分有Author

我们希望为MoviesBooks分配标签,但不使用MovieTagBookTag型号,而是使用GFKMovieBook的单个TaggedItem类.

以下是模型 struct :

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 relation来计算TaggedItem中的related data,我们会得到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)(从querysetprefetch多个级别,包含不同类型的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.

现在想象一下这样的情况,我们想要的不仅仅是books与他们的author,还有movies与他们的director.这里有几次try .

愚蠢的方式:

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

>>> 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 报告过任何事情,所以这就是为什么我先在这里问的原因.我无法跟踪错误,无法自己决定这是一个错误,还是可以实现的功能.

推荐答案

prefetch_related_objects人获救.

从Django 1.(Note: it still presents in the previous versions, but was not part of the public API.)01开始,我们可以使用prefetch_related_objects来分而治之.

prefetch_related是一个操作,在该操作中,Django获取已判断的查询集的相关数据after(在判断主要查询之后执行第二个查询).为了正常工作,它要求查询集中的项是同构的(相同类型).现在反向泛型生成不起作用的主要原因是,我们有来自不同内容类型的对象,而代码还不够智能,不能将不同内容类型的流分开.

现在使用prefetch_related_objects,我们只对subset个查询集进行提取,其中所有项都是同构的.下面是一个示例:

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

Django相关问答推荐

Django-将html数据保存到quillfield中

Django:不能在子函数中返回重定向异常.第一个功能继续

一次请求中更新整个Django模型

Django 相当于子查询

在Django测试get方法中获取HttpResponseNotFound

在 Django 4.1 中提交表单之前显示数据

Django ORM:子查询上的文本聚合器

Django 过滤器:过滤隐藏在多层模型 struct 中的对象

如何将 ManyToManyField 与另一个 ManyToManyField 进行比较

设置 SECURE_HSTS_SECONDS 可以不可逆转地 destruct 您的网站?

如何将表单中的用户字段设置为当前登录的用户?

Django基于类的视图上的success_url的反向抱怨循环导入

直接在模型类上使用 Django 管理器与静态方法

可插拔应用程序的Django默认设置约定?

InvalidBasesError:无法解析 [] 的基础

Apache + mod_wsgi 与 nginx + gunicorn

django 模板中对象的模型名称

django 管理员操作而不 Select 对象

使用 request.user 的 Django 和中间件始终是匿名的

Django:在模块中实现 status字段的最佳方式