Подтвердить что ты не робот

Предполагается, что django prefetch_related работает с GenericRelation

ОБНОВЛЕНИЕ: Open Ticked об этой проблеме: 24272

О чем все?

Django имеет класс GenericRelation, который добавляет "обратные" родовые отношения для включения дополнительного API.

Оказывается, мы можем использовать это reverse-generic-relation для filtering или ordering, но мы не можем использовать его внутри prefetch_related.

Мне было интересно, если это ошибка, или она не должна работать, или это что-то, что может быть реализовано в функции.

Позвольте мне показать вам на некоторых примерах, что я имею в виду.

Допустим, у нас есть две основные модели: Movies и Books.

  • Movies есть Director
  • Books имеют Author

И мы хотим, чтобы присвоить теги наших Movies и Books, но вместо того, чтобы использовать 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()
[<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 TaggedItem некоторые related data TaggedItem через это 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) 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) (для prefetch многих уровней из queryset содержащего различные типы 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. Я использую Django 1.7.3 и Python 2.7.6. И мне любопытно, почему Джанго выбрасывает эту ошибку? Почему Django ищет object_id в модели 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()

Но здесь все по-другому. Джанго на самом деле пытается разрешить отношения... и терпит неудачу. Это ошибка, о которой следует сообщить? Я никогда ничего не сообщал Джанго, поэтому сначала спрашиваю здесь. Я не могу отследить ошибку и решить для себя, является ли это ошибкой или функцией, которая может быть реализована.

4b9b3361

Ответ 1

Если вы хотите извлечь экземпляры Book и выполнить предварительную выборку связанных тегов, используйте Book.objects.prefetch_related('tags'). Нет необходимости использовать обратное соотношение здесь.

Вы также можете посмотреть связанные тесты в исходном коде Django.

Также в документации Django указано, что prefetch_related() должен работать с GenericForeignKey и GenericRelation:

prefetch_related, с другой стороны, выполняет отдельный поиск для каждой связи и выполняет "соединение в Python. Это позволяет ему предварительно выбирать объекты" многие-ко-многим "и" многие-к-одному ", которые не могут быть выполнены с помощью select_related, в дополнение к внешним ключам и отношениям" один-к-одному", которые поддерживаются select_related. Он также поддерживает предварительную выборку GenericRelation и GenericForeignKey.

ОБНОВЛЕНИЕ: Для предварительной выборки content_object для TaggedItem вы можете использовать TaggedItem.objects.all().prefetch_related('content_object'), если вы хотите ограничить результат только тегами Book объектов, которые вы можете дополнительно фильтровать для ContentType (не уверен, что prefetch_related работает с related_query_name). Если вы также хотите получить Author вместе с книгой, вам нужно использовать select_related() не prefetch_related(), так как это отношение ForeignKey, вы можете объедините это в пользовательский prefetch_related() запрос:

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

Ответ 2

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