Comment combiner 2 ou plusieurs querysets dans une vue Django?
J'essaie de construire la recherche d'un site Django que je construis, et dans la recherche je recherche dans 3 modèles différents. Et pour obtenir la pagination sur la liste de résultats de recherche, je voudrais utiliser une vue object_list générique pour afficher les résultats. Mais pour ce faire, je dois fusionner 3 querysets en un seul.
Comment puis-je faire ça? J'ai essayé ceci:
result_list = []
page_list = Page.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
article_list = Article.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
post_list = Post.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
for x in page_list:
result_list.append(x)
for x in article_list:
result_list.append(x)
for x in post_list:
result_list.append(x)
return object_list(
request,
queryset=result_list,
template_object_name='result',
paginate_by=10,
extra_context={
'search_term': search_term},
template_name="search/result_list.html")
, Mais cela ne fonctionne pas, j'obtiens une erreur lorsque je tente d'utiliser cette liste dans la vue générique. La liste manque le clone attribut.
Ce que Quelqu'un sait comment je peux fusionner les trois listes, page_list
, article_list
et post_list
?
11 réponses
Concaténer les querysets en une liste est l'approche la plus simple. Si la base de données sera touchée pour tous les querysets de toute façon (par exemple parce que le résultat doit être trié), cela n'ajoutera pas de coût supplémentaire.
from itertools import chain
result_list = list(chain(page_list, article_list, post_list))
Utiliser itertools.chain
est plus rapide que de boucler chaque liste et d'ajouter des éléments un par un, puisque itertools
est implémenté en C. Il consomme également moins de mémoire que de convertir chaque queryset en une liste avant de concaténer.
Maintenant, il est possible de trier la liste résultante par exemple par date (as demandé dans le commentaire de hasen j à une autre réponse). La fonction sorted()
Accepte commodément un générateur et renvoie une liste:
result_list = sorted(
chain(page_list, article_list, post_list),
key=lambda instance: instance.date_created)
Si vous utilisez Python 2.4 ou version ultérieure, vous pouvez utiliser attrgetter
au lieu d'un lambda. Je me souviens avoir lu à ce sujet étant plus rapide, mais je n'ai pas vu une différence de vitesse notable pour une liste de millions d'articles.
from operator import attrgetter
result_list = sorted(
chain(page_list, article_list, post_list),
key=attrgetter('date_created'))
Vous pouvez utiliser la classe QuerySetChain
ci-dessous. Lorsque vous l'utilisez avec le paginator de Django, il ne doit frapper la base de données qu'avec des requêtes COUNT(*)
pour tous les jeux de requêtes et des requêtes SELECT()
uniquement pour les jeux de requêtes dont les enregistrements sont affichés sur la page en cours.
Notez que vous devez spécifier template_name=
Si vous utilisez un QuerySetChain
avec des vues génériques, même si les ensembles de requêtes chaînés utilisent tous le même modèle.
from itertools import islice, chain
class QuerySetChain(object):
"""
Chains multiple subquerysets (possibly of different models) and behaves as
one queryset. Supports minimal methods needed for use with
django.core.paginator.
"""
def __init__(self, *subquerysets):
self.querysets = subquerysets
def count(self):
"""
Performs a .count() for all subquerysets and returns the number of
records as an integer.
"""
return sum(qs.count() for qs in self.querysets)
def _clone(self):
"Returns a clone of this queryset chain"
return self.__class__(*self.querysets)
def _all(self):
"Iterates records in all subquerysets"
return chain(*self.querysets)
def __getitem__(self, ndx):
"""
Retrieves an item or slice from the chained set of results from all
subquerysets.
"""
if type(ndx) is slice:
return list(islice(self._all(), ndx.start, ndx.stop, ndx.step or 1))
else:
return islice(self._all(), ndx, ndx+1).next()
Dans votre exemple, l'utilisation serait:
pages = Page.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
articles = Article.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
posts = Post.objects.filter(Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term) |
Q(tags__icontains=cleaned_search_term))
matches = QuerySetChain(pages, articles, posts)
Ensuite, utilisez matches
avec le paginateur comme vous avez utilisé result_list
dans votre exemple.
Le module itertools
a été introduit dans Python 2.3, il devrait donc être disponible dans toutes les versions de Python sur lesquelles Django s'exécute.
Connexes, pour mélanger des ensembles de requêtes du même modèle, ou pour des champs similaires de quelques modèles, en commençant par Django 1.11 a qs.union()
la méthode est également disponible:
union()
union(*other_qs, all=False)
Nouveau dans Django 1.11. Utilise L'opérateur UNION de SQL pour combiner les résultats de deux ou plusieurs jeux de requêtes. Par exemple:
>>> qs1.union(qs2, qs3)
L'opérateur UNION ne sélectionne que des valeurs distinctes par défaut. Pour autoriser les valeurs en double, utilisez all=True argument.
Union (), intersection () et difference() renvoient des instances de modèle de le type du premier QuerySet même si les arguments sont des QuerySets de d'autres modèles. Passer différents modèles fonctionne aussi longtemps que le SELECT la liste est la même dans tous les QuerySets (au moins les types, les noms ne le font pas d'importance aussi longtemps que les types dans le même ordre).
En outre, seuls LIMIT, OFFSET et ORDER BY (c'est-à-dire order_by()) sont autorisés sur le QuerySet. En outre, les bases de données imposer des restrictions sur les opérations autorisées dans le combiné requête. par exemple, la plupart des bases de données n'autorisent pas les requêtes combinées.
Https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union
Le gros inconvénient de votre approche actuelle est son inefficacité avec de grands ensembles de résultats de recherche, car vous devez extraire l'ensemble des résultats de la base de données à chaque fois, même si vous n'avez l'intention d'afficher qu'une page de résultats.
Afin de ne retirer que les objets dont vous avez réellement besoin de la base de données, vous devez utiliser la pagination sur un QuerySet, pas une liste. Si vous faites cela, Django découpe réellement le jeu de requêtes avant l'exécution de la requête, de sorte que la requête SQL utilisera OFFSET et Limite pour obtenir uniquement les enregistrements que vous afficherez réellement. Mais vous ne pouvez pas le faire à moins que vous ne puissiez entasser votre recherche dans une seule requête en quelque sorte.
Étant donné que vos trois modèles ont des champs title et body, pourquoi ne pas utiliser model inheritance ? Il suffit que les trois modèles héritent d'un ancêtre commun qui a un titre et un corps, et effectuez la recherche en tant que requête unique sur le modèle ancêtre.
Si vous voulez enchaîner beaucoup de querysets, essayez ceci:
from itertools import chain
result = list(chain(*docs))
Où: docs est une liste de querysets
DATE_FIELD_MAPPING = {
Model1: 'date',
Model2: 'pubdate',
}
def my_key_func(obj):
return getattr(obj, DATE_FIELD_MAPPING[type(obj)])
And then sorted(chain(Model1.objects.all(), Model2.objects.all()), key=my_key_func)
Cité de https://groups.google.com/forum/#!topic/django-users/6wUNuJa4jVw. Voir Alex Gaynor
On dirait que t_rybik a créé une solution complète à http://www.djangosnippets.org/snippets/1933/
Pour la recherche, il est préférable d'utiliser des solutions dédiées comme botte de Foin - c'est très souple.
Voici une idée... il suffit de tirer vers le bas une pleine page de résultats de chacun des trois, puis jeter les 20 moins utiles... cela élimine les grands querysets et de cette façon vous ne sacrifiez qu'un peu de performance au lieu de beaucoup
Exigences:
Django==2.0.2
, django-querysetsequence==0.8
Dans le cas où vous souhaitez combiner querysets
et viennent toujours avec un QuerySet
, vous pourriez vouloir vérifier django-queryset-séquence.
Mais une note à ce sujet. Il ne faut que deux querysets
comme argument. Mais avec python reduce
vous pouvez toujours l'appliquer à plusieurs queryset
s.
from functools import reduce
from queryset_sequence import QuerySetSequence
combined_queryset = reduce(QuerySetSequence, list_of_queryset)
Et c'est tout. Ci-dessous est une situation que j'ai rencontré et comment j'ai employé list comprehension
, reduce
et django-queryset-sequence
from functools import reduce
from django.shortcuts import render
from queryset_sequence import QuerySetSequence
class People(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
mentor = models.ForeignKey('self', null=True, on_delete=models.SET_NULL, related_name='my_mentees')
class Book(models.Model):
name = models.CharField(max_length=20)
owner = models.ForeignKey(Student, on_delete=models.CASCADE)
# as a mentor, I want to see all the books owned by all my mentees in one view.
def mentee_books(request):
template = "my_mentee_books.html"
mentor = People.objects.get(user=request.user)
my_mentees = mentor.my_mentees.all() # returns QuerySet of all my mentees
mentee_books = reduce(QuerySetSequence, [each.book_set.all() for each in my_mentees])
return render(request, template, {'mentee_books' : mentee_books})