web-dev-qa-db-ja.com

Django prefetch_relatedはGenericRelationで動作するはずです

UPDATE:この問題について言及された公開: 24272

何が何ですか?

Djangoには GenericRelation クラスがあり、「逆」の一般的な関係を追加して、追加の[〜#〜] apiを有効にします[〜#〜]

この_reverse-generic-relation_はfilteringまたはorderingに使用できますが、_prefetch_related_内では使用できません。

これはバグなのか、それとも機能しないのか、または機能に実装できるものなのかと思っていました。

いくつかの例を挙げて説明します。

MoviesBooksの2つのメインモデルがあるとします。

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

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>]
_

しかし、この_related data_を介してprefetchTaggedItem some _reverse generic relation_を実行しようとすると、AttributeErrorが取得されます。

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

ここでbooksの代わりに_content_object_を使用しないのはなぜですか?その理由は、これが必要なのは次の場合にのみ機能するためです。

1)prefetchquerysetsから1レベルだけ深く、異なるタイプの_content_object_が含まれています。

_>>> TaggedItem.objects.all().prefetch_related('content_object')
[<TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: roman>, <TaggedItem: action movie>]
_

2)prefetch多くのレベルですが、_content_object_の1つのタイプのみを含むquerysetsから。

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

しかし、1)と2)の両方が必要な場合(prefetchから_content_objects_の異なるタイプを含むquerysetの多くのレベルに)、_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を持っています。

ここで、prefetchbooksだけでなく、authormoviesdirectorにしたい状況を想像してください。ここにいくつかの試みがあります。

ばかげた方法:

_>>> 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がそのエラーをスローしているのか興味がありますか? Bookモデルで_Djangoが_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には何も報告したことがないので、最初にここで質問します。エラーを追跡して、これがバグなのか、実装可能な機能なのかを自分で判断することができません。

33
Todor

prefetch_related_objectsが助けになりました。

Django 1.10から開始します(注:以前のバージョンにも存在しますが、パブリックAPIの一部ではありませんでした。)prefetch_related_objects は、問題を分割して克服します。

prefetch_relatedは操作です。ここでDjangoは関連データをフェッチしますafterクエリセットが評価されました(メインのクエリが評価された後に2番目のクエリを実行します)。そして、機能するためには、クエリセットの項目が同種(同じタイプ)であることが期待されます。逆ジェネリック生成が現在機能しない主な理由は、異なるコンテンツタイプのオブジェクトがあり、コードがまだないためです。異なるコンテンツタイプのフローを分離するのに十分なほどスマートです。

ここで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)
    )
2
Todor

Bookインスタンスを取得して関連タグをプリフェッチする場合は、Book.objects.prefetch_related('tags')を使用します。ここでは逆の関係を使用する必要はありません。

Djangoソースコード で関連するテストを確認することもできます。

また、 Djangoのドキュメント は、prefetch_related()GenericForeignKeyおよびGenericRelationで動作するはずであると述べています。

一方、_prefetch_related_は、関係ごとに個別のルックアップを行い、Pythonで「結合」を行います。これにより、select_relatedでサポートされている外部キーと1対1の関係に加えて、select_relatedを使用して実行できない多対多および多対1のオブジェクトをプリフェッチできます。 GenericRelationおよびGenericForeignKeyのプリフェッチもサポートしています。

UPDATE:TaggedItemの_content_object_をプリフェッチするには、必要に応じてTaggedItem.objects.all().prefetch_related('content_object')を使用できます。結果をタグ付けされたBookオブジェクトのみに制限するには、ContentTypeをさらにフィルタリングできます(_prefetch_related_が_related_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')
    )
)
_
28