web-dev-qa-db-fra.com

Django prefetch_related est-il censé fonctionner avec GenericRelation?

UPDATE: Un sujet ouvert sur ce problème: 24272

De quoi s'agit-il?

Django a une classe GenericRelation class, qui ajoute une relation générique «inversée» pour permettre un API supplémentaire. 

Il s'avère que nous pouvons utiliser ce reverse-generic-relation pour filtering ou ordering, mais nous ne pouvons pas l'utiliser dans prefetch_related.

Je me demandais s'il s'agissait d'un bogue ou si cela ne devait pas fonctionner, ou si quelque chose qui pouvait être implémenté dans la fonctionnalité. 

Laissez-moi vous montrer avec quelques exemples ce que je veux dire.

Disons que nous avons deux modèles principaux: Movies et Books.

  • Movies ont un Director
  • Books ont un Author

Et nous voulons affecter des balises à nos Movies et Books, mais au lieu d'utiliser les modèles MovieTag et BookTag, nous voulons utiliser une seule classe TaggedItem avec un GFK à Movie ou Book.

Voici la structure du modèle:

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

Et quelques données initiales:

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

Donc, en tant que docs , nous pouvons faire ce genre de choses.

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

Mais si nous essayons de prefetch certains related data de TaggedItem via ce reverse generic relation, nous allons obtenir un AttributeError.

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

Certains d'entre vous se demandent peut-être pourquoi je n'utilise simplement pas content_object au lieu de books ici? La raison en est, parce que cela ne fonctionne que lorsque nous voulons:

1) prefetch seulement un niveau de profondeur de querysets contenant un type différent de content_object

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

2) prefetch plusieurs niveaux mais à partir de querysets contenant un seul type de content_object.

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

Mais, si nous voulons les deux 1) et 2) (pour prefetch plusieurs niveaux de queryset contenant différents types de content_objects, nous ne pouvons pas utiliser content_object.

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

Django pense que tous les content_objects sont Books et qu'ils ont donc un Author.

Imaginons maintenant la situation dans laquelle nous voulons prefetch non seulement le books avec son author, mais également le movies avec son director. Voici quelques tentatives. 

La façon idiote:

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

Peut-être avec un objet personnalisé 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.

Quelques solutions de ce problème sont montrées ici . Mais c’est beaucoup de travail sur les données que je veux éviter… .J’aime beaucoup l’API provenant du reversed generic relations, il serait très agréable de pouvoir faire prefetchs comme cela:

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

Ou comme ça:

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

Mais comme vous pouvez le constater, nous obtenons toujours ce AttributeError . J'utilise Django 1.7.3 et Python 2.7.6. Et je suis curieux de savoir pourquoi Django lance cette erreur? Pourquoi Django recherche-t-il un object_id dans le modèle Book? Pourquoi je pense que cela pourrait être un bug? Généralement, lorsque nous demandons à prefetch_related de résoudre un problème qu'il ne peut pas résoudre, nous voyons:

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

Mais ici, c'est différent. Django essaye réellement de résoudre la relation ... et échoue. Est-ce un bug qui devrait être signalé? Je n'ai jamais rien rapporté à Django, c'est pourquoi je pose la question en premier. Je suis incapable de localiser l'erreur et de décider par moi-même s'il s'agit d'un bogue ou d'une fonctionnalité pouvant être implémentée.

30
Todor

Si vous souhaitez récupérer des instances Book et extraire les balises associées, utilisez Book.objects.prefetch_related('tags'). Pas besoin d'utiliser la relation inverse ici.

Vous pouvez également consulter les tests correspondants dans le code source de Django .

La documentation Django indique également que prefetch_related() est supposé fonctionner avec GenericForeignKey et GenericRelation:

prefetch_related, en revanche, effectue une recherche distincte pour chaque relation et effectue la "jointure" en Python. Cela lui permet de pré-extraire des objets plusieurs-à-plusieurs et plusieurs-un, ce qui ne peut pas être fait avec select_related, en plus de la clé étrangère et des relations un-à-un prises en charge par select_related. Il prend également en charge la pré-extraction de GenericRelation et GenericForeignKey.

UPDATE: Pour extraire le content_object d'une TaggedItem, vous pouvez utiliser TaggedItem.objects.all().prefetch_related('content_object'). Si vous souhaitez limiter le résultat aux seuls objets Book marqués, vous pouvez également filtrer pour la ContentType (vous ne savez pas si prefetch_related fonctionne avec le related_query_name). Si vous souhaitez également obtenir la variable Author avec le livre, vous devez utiliser select_related() pas prefetch_related() puisqu'il s'agit d'une relation ForeignKey, vous pouvez combiner cette opération dans une requête custom 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')
    )
)
28
Bernhard Vallant

prefetch_related_objects à la rescousse.

À partir de Django 1.10 (Remarque: il existe toujours dans les versions précédentes, mais ne faisait pas partie de l'API publique.), Nous pouvons utiliser prefetch_related_objects pour diviser et résoudre notre problème.

prefetch_related est une opération au cours de laquelle Django récupère les données associées après le jeu de requêtes a été évalué (en effectuant une seconde requête après l'évaluation de la requête principale). Et pour que cela fonctionne, les éléments de la requête doivent être homogènes (du même type). La principale raison pour laquelle la génération générique inverse ne fonctionne pas actuellement est que nous avons des objets de différents types de contenu et que le code n'est pas encore assez intelligent pour séparer le flux pour différents types de contenu.

Maintenant, en utilisant prefetch_related_objects, nous effectuons des extractions uniquement sur un sous-ensemble de notre ensemble de requêtes, où tous les éléments seront homogènes. Voici un exemple:

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