wake-up-neo.net

Soll Django prefetch_related mit GenericRelation zusammenarbeiten

UPDATE: Ein offenes Häkchen zu diesem Problem: 24272

Worum geht es?

Django hat eine GenericRelation Klasse, die eine generische "reverse" Beziehung hinzufügt, um eine zusätzliche API zu aktivieren. 

Es stellt sich heraus, dass wir diese reverse-generic-relation für filtering oder ordering verwenden können, aber wir können sie nicht in prefetch_related verwenden.

Ich habe mich gefragt, ob dies ein Fehler ist oder nicht funktionieren sollte oder ob es etwas ist, das in der Funktion implementiert werden kann. 

Lassen Sie mich anhand einiger Beispiele zeigen, was ich meine.

Nehmen wir an, wir haben zwei Hauptmodelle: Movies und Books.

  • Movies hat eine Director
  • Books hat eine Author

Und wir möchten unseren Movies und Books Tags zuweisen, aber anstatt MovieTag und BookTag zu verwenden, möchten wir eine einzige TaggedItem Klasse mit verwenden a GFK bis Movie oder Book.

Hier ist die Modellstruktur:

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

Und ein paar Anfangsdaten:

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

So wie die docs show können wir so etwas machen.

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

Wenn wir jedoch versuchen, prefetch einige related data von TaggedItem über diese reverse generic relation zu erstellen, erhalten wir einen AttributeError.

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

Einige von Ihnen fragen sich vielleicht, warum ich hier nicht content_object statt books verwende? Der Grund ist, weil dies nur funktioniert, wenn wir wollen:

1) prefetch nur eine Ebene tief von querysets, die verschiedene Typen von content_object enthält. 

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

2) prefetch viele Ebenen, aber aus querysets, die nur einen Typ von content_object enthält.

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

Wenn wir jedoch sowohl 1) als auch 2) (um prefetch viele Ebenen von queryset mit verschiedenen Arten von content_objects enthalten möchten, können wir content_object nicht verwenden.

>>> TaggedItem.objects.all().prefetch_related('content_object__author')
Traceback (most recent call last):
  ...
AttributeError: 'Movie' object has no attribute 'author_id'

Django glaubt, dass alle content_objectsBooks sind, und daher haben sie eine Author.

Stellen Sie sich nun die Situation vor, in der wir prefetch nicht nur die books mit ihren author wollen, sondern auch die movies mit ihren director. Hier sind einige Versuche. 

Der dumme Weg:

>>> TaggedItem.objects.all().prefetch_related(
...     'content_object__author',
...     'content_object__director',
... )
Traceback (most recent call last):
  ...
AttributeError: 'Movie' object has no attribute 'author_id'

Vielleicht mit einem benutzerdefinierten Prefetch-Objekt?

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

Einige Lösungen dieses Problems werden gezeigt hier . Aber das ist eine Menge Massage über die Daten, die ich vermeiden möchte ... Ich mag die API wirklich, die von reversed generic relations kommt, es wäre sehr schön, prefetchs so machen zu können:

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

Oder so:

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

Aber wie Sie sehen, bekommen wir immer AttributeError . Ich verwende Django 1.7.3 und Python 2.7.6. Und ich bin neugierig, warum Django diesen Fehler wirft. Warum sucht Django nach einem object_id im Book-Modell? Warum denke ich, dass dies ein Fehler sein kann? Normalerweise, wenn wir prefetch_related bitten, etwas aufzulösen, was nicht möglich ist, sehen wir:

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

Aber hier ist es anders. Django versucht tatsächlich, die Beziehung aufzulösen ... und scheitert. Ist das ein Fehler, der gemeldet werden sollte? Ich habe Django nie etwas gemeldet, deshalb frage ich hier zuerst. Ich kann den Fehler nicht aufspüren und nicht selbst entscheiden, ob es sich um einen Fehler handelt oder eine Funktion, die implementiert werden könnte.

30
Todor

Wenn Sie Book-Instanzen abrufen und die zugehörigen Tags vorab holen möchten, verwenden Sie Book.objects.prefetch_related('tags'). Die umgekehrte Beziehung muss hier nicht verwendet werden.

Sie können sich auch die zugehörigen Tests im Django-Quellcode ansehen.

Auch die Django-Dokumentation besagt, dass prefetch_related() mit GenericForeignKey und GenericRelation zusammenarbeiten soll:

prefetch_related hingegen führt für jede Beziehung eine separate Suche aus und führt die "Verbindung" in Python durch. Dies ermöglicht es, viele-zu-viele- und viele-zu-eins-Objekte vorab abzurufen, was mithilfe von select_related nicht möglich ist, zusätzlich zu den Fremdschlüssel- und Eins-zu-eins-Beziehungen, die von select_related unterstützt werden. Es unterstützt auch das Prefetching von GenericRelation und GenericForeignKey.

UPDATE: Um den content_object für eine TaggedItem abzurufen, können Sie TaggedItem.objects.all().prefetch_related('content_object') verwenden. Wenn Sie das Ergebnis auf nur markierte Book-Objekte beschränken möchten, können Sie zusätzlich nach ContentType filtern (nicht sicher, ob prefetch_related mit dem related_query_name funktioniert). Wenn Sie die Author auch zusammen mit dem Buch erhalten möchten, müssen Sie select_related() nicht prefetch_related() verwenden, da dies eine ForeignKey-Beziehung ist. Sie können diese in einer benutzerdefinierten prefetch_related()-Abfrage kombinieren.

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

prefetch_related_objects zur Rettung.

Ab Django 1.10 (Hinweis: Es ist noch in den vorherigen Versionen vorhanden, war aber nicht Teil der öffentlichen API.), Können wir prefetch_related_objects verwenden, um unser Problem zu teilen und zu überwinden.

prefetch_related ist eine Operation, bei der Django verwandte Daten abruft, nachdem das Abfrageset ausgewertet wurde (wobei eine zweite Abfrage ausgeführt wird, nachdem das Hauptset ausgewertet wurde). Und um zu funktionieren, erwartet er, dass die Elemente im Abfrageset homogen sind (derselbe Typ). Der Hauptgrund, warum die Generierung der umgekehrten Generierung derzeit nicht funktioniert, besteht darin, dass wir Objekte mit unterschiedlichen Inhaltstypen haben und der Code noch nicht intelligent genug ist, um den Fluss für verschiedene Inhaltstypen zu trennen.

Mit prefetch_related_objects holen wir jetzt nur noch ein subset unseres Abfragesets ab, in dem alle Elemente homogen sind. Hier ist ein Beispiel:

from Django.db import models
from Django.db.models.query import prefetch_related_objects
from Django.contrib.contenttypes.models import ContentType

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)
prefetch_related_objects([item for item in page.object_list if item.content_type = book_ct],
    models.Prefetch('content_object', queryset=Book.objects.all().select_related('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)
prefetch_related_objects([item for item in page.object_list if item.content_type = movie_ct],
    models.Prefetch('content_object', queryset=Movie.objects.all().select_related('director'),
)

# This will make 3 queries in total
# 1 for page items
# 1 for books 
# 1 for movies
# 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
0
Todor