Comment combiner 2 ou plusieurs ensembles de requêtes dans une vue Django?
-
08-07-2019 - |
Question
J'essaie de construire la recherche d'un site Django que je construis, et dans la recherche, je cherche dans 3 modèles différents. Et pour obtenir une pagination dans la liste des résultats de la recherche, j'aimerais utiliser une vue générique object_list pour afficher les résultats. Mais pour ce faire, je dois fusionner 3 ensembles de requêtes en un.
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, une erreur survient lorsque j'essaie d'utiliser cette liste dans la vue générique. L'attribut clone manque dans la liste.
Quelqu'un sait-il que je peux fusionner les trois listes, liste_page
, liste article
et liste_post
?
La solution
La concaténation des ensembles de requêtes dans une liste est l’approche la plus simple. Si de toute façon la base de données est touchée pour tous les ensembles de requêtes (par exemple, parce que le résultat doit être trié), cela n'augmentera pas les coûts.
from itertools import chain
result_list = list(chain(page_list, article_list, post_list))
L'utilisation de itertools.chain
est plus rapide que de boucler chaque liste et d'ajouter des éléments l'un après l'autre, car itertools
est implémenté en C. Il consomme également moins de mémoire que la conversion de chaque ensemble de requêtes. dans une liste avant de concaténer.
Il est maintenant possible de trier la liste résultante, par exemple. par date (comme demandé dans le commentaire de hasen j avec une autre réponse). La fonction trié ()
accepte facilement 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 une version ultérieure, vous pouvez utiliser attrgetter
au lieu d'un lambda. Je me souviens d’avoir lu que c’était plus rapide, mais je ne voyais pas de différence de vitesse notable pour une liste d’articles de plus en plus volumineuse.
from operator import attrgetter
result_list = sorted(
chain(page_list, article_list, post_list),
key=attrgetter('date_created'))
Autres conseils
Essayez ceci:
matches = pages | articles | posts
Il conserve toutes les fonctions des ensembles de requêtes, ce qui est bien si vous souhaitez order_by
ou similaire.
Remarque: cela ne fonctionne pas sur les ensembles de requêtes de deux modèles différents.
Connexes, pour mélanger des ensembles de requêtes du même modèle ou pour des champs similaires de quelques modèles, à partir de Django 1.11 a La méthode qs.union ()
est également disponible:
union ()
union(*other_qs, all=False)
Nouveauté de Django 1.11 . Utilise l’opérateur UNION de SQL pour combiner les résultats de deux QuerySets ou plus. Par exemple:
>>> qs1.union(qs2, qs3)
L'opérateur UNION ne sélectionne que des valeurs distinctes par défaut. Pour autoriser les doublons, utilisez la valeur 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 of d'autres modèles. Passer différents modèles fonctionne aussi longtemps que SELECT La liste est la même dans tous les QuerySets (au moins les types, les noms ne sont pas importe tant que les types dans le même ordre).
De plus, seuls LIMIT, OFFSET et ORDER BY (c.-à-d. trancher et order_by ()) est autorisé sur le QuerySet résultant. En outre, bases de données imposer des restrictions sur les opérations autorisées dans la combinaison requêtes. Par exemple, la plupart des bases de données n'autorisent pas LIMIT ou OFFSET dans les requêtes combinées.
https: / /docs.djangoproject.com/fr/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union
Vous pouvez utiliser la classe QuerySetChain
ci-dessous. Lorsqu'il est utilisé avec la pagination de Django, il ne doit toucher la base de données qu'avec des requêtes COUNT (*)
pour tous les ensembles de requêtes et SELECT ()
uniquement pour les ensembles de requêtes dont les enregistrements sont affichés. la page en cours.
Notez que vous devez spécifier nom_modèle =
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)
Utilisez ensuite pour faire correspondre
au 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 Python sur lesquelles Django est exécuté.
Le principal inconvénient de votre approche actuelle est son inefficacité avec des ensembles de résultats de recherche volumineux, car vous devez extraire l'ensemble des résultats dans la base de données à chaque fois, même si vous ne souhaitez afficher qu'une seule page de résultats.
Pour extraire uniquement les objets dont vous avez réellement besoin de la base de données, vous devez utiliser la pagination sur un ensemble de requêtes, pas une liste. Si vous faites cela, Django coupe le QuerySet avant l’exécution de la requête. La requête SQL utilisera donc OFFSET et LIMIT pour n’obtenir que les enregistrements à afficher. Mais vous ne pouvez le faire que si vous pouvez en quelque sorte regrouper votre recherche en une seule requête.
Étant donné que vos trois modèles ont des champs de titre et de corps, pourquoi ne pas utiliser héritage de modèle ? Il suffit de faire hériter les trois modèles d'un ancêtre commun qui a un titre et un corps et d'effectuer la recherche en tant que requête unique sur le modèle ancêtre.
Si vous souhaitez enchaîner de nombreux ensembles de requêtes, essayez ceci:
from itertools import chain
result = list(chain(*docs))
où: docs est une liste de jeux de requêtes
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é dans https://groups.google.com/forum/#!topic/django -utilisateurs / 6wUNuJa4jVw . Voir Alex Gaynor
.voici une idée ... il suffit d'extraire une page complète de résultats pour chacun des trois, puis d'éliminer les 20 moins utiles ... ceci élimine les gros ensembles de requêtes et vous permet de sacrifier un peu les performances au lieu de beaucoup
Conditions requises:
Django == 2.0.2
, django-querysetsequence == 0.8
Si vous souhaitez combiner jeux de requêtes
tout en obtenant un QuerySet
, vous pouvez extraire séquence django-queryset-sequence .
Mais une note à ce sujet. Il ne faut que deux querysets
en tant qu'argument. Mais avec
réduire
de python, vous pouvez toujours l’appliquer à plusieurs requêtesset
.
from functools import reduce
from queryset_sequence import QuerySetSequence
combined_queryset = reduce(QuerySetSequence, list_of_queryset)
Et c'est tout. Voici une situation que j'ai rencontrée et comment j'ai utilisé la compréhension de liste
, réduire
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})
Cela peut être réalisé de deux manières.
Première façon de procéder
Utilisez l'opérateur d'union pour queryset |
pour prendre l'union de deux queryset. Si les deux ensembles de requêtes appartiennent au même modèle / modèle unique, il est possible de combiner des ensembles de requêtes à l'aide de l'union.
Pour une instance
pagelist1 = Page.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
pagelist2 = Page.objects.filter(
Q(title__icontains=cleaned_search_term) |
Q(body__icontains=cleaned_search_term))
combined_list = pagelist1 | pagelist2 # this would take union of two querysets
2ème façon de procéder
Une autre méthode permettant de combiner deux requêtes consiste à utiliser la fonction de chaîne itertools .
from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Cette fonction récursive concatène un tableau de jeux de requêtes en un seul.
def merge_query(ar):
if len(ar) ==0:
return [ar]
while len(ar)>1:
tmp=ar[0] | ar[1]
ar[0]=tmp
ar.pop(1)
return ar