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.
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 deGenericRelation
etGenericForeignKey
.
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')
)
)
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