Comment combiner deux ou plusieurs ensembles de requêtes dans une vue Django?

654

J'essaie de construire la recherche d'un site Django que je suis en train de construire, et dans cette recherche, je cherche dans 3 modèles différents. Et pour obtenir la pagination sur la liste des résultats de recherche, je voudrais 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 seul.

Comment puis je faire ça? J'ai essayé ça:

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 ça ne marche pas. J'obtiens une erreur lorsque j'essaie d'utiliser cette liste dans la vue générique. La liste ne contient pas l'attribut clone.

Est-ce que quelqu'un sait comment je peux fusionner les trois listes page_list, article_listet post_list?

espenhogbakk
la source
On dirait que t_rybik a créé une solution complète sur djangosnippets.org/snippets/1933
akaihola
Pour la recherche, il est préférable d'utiliser des solutions dédiées comme Haystack - c'est très flexible.
gardien
1
Utilisateurs de Django 1.11 et abv, voir cette réponse - stackoverflow.com/a/42186970/6003362
Sahil Agarwal
Remarque : la question se limite au cas très rare où après avoir fusionné 3 modèles différents ensemble, vous n'avez pas besoin d'extraire à nouveau les modèles sur la liste pour distinguer les données sur les types. Dans la plupart des cas - si une distinction est attendue - il y aura une mauvaise interface. Pour les mêmes modèles: voir réponses sur union.
Sławomir Lenart

Réponses:

1058

La concaténation des ensembles de requêtes dans une liste est l'approche la plus simple. Si la base de données est de toute façon atteinte pour tous les ensembles de requêtes (par exemple parce que le résultat doit être trié), cela n'augmentera pas le coût.

from itertools import chain
result_list = list(chain(page_list, article_list, post_list))

L'utilisation itertools.chainest plus rapide que la mise en boucle de chaque liste et l'ajout d'éléments un par un, car elle itertoolsest implémentée en C. Elle consomme également moins de mémoire que la conversion de chaque ensemble de requêtes en liste avant la concaténation.

Il est maintenant possible de trier la liste résultante par exemple par date (comme demandé dans le commentaire de hasen j à une autre réponse). La sorted()fonction accepte facilement un générateur et retourne 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 à la attrgetterplace d'un lambda. Je me souviens avoir lu que c'était plus rapide, mais je n'ai pas vu de différence de vitesse notable pour un million d'articles.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))
akaihola
la source
14
Si vous fusionnez des ensembles de requêtes de la même table pour effectuer une requête OR et que vous avez des lignes en double, vous pouvez les éliminer avec la fonction groupby: from itertools import groupby unique_results = [rows.next() for (key, rows) in groupby(result_list, key=lambda obj: obj.id)]
Josh Russo
1
Ok, donc nm sur la fonction groupby dans ce contexte. Avec la fonction Q, vous devriez pouvoir effectuer n'importe quelle requête OU dont vous avez besoin: https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects
Josh Russo
2
@apelliciari Chain utilise beaucoup moins de mémoire que list.extend, car il n'a pas besoin de charger complètement les deux listes en mémoire.
Dan Gayle
2
@AWrightIV Voici la nouvelle version de ce lien: docs.djangoproject.com/en/1.8/topics/db/queries/…
Josh Russo
1
essayer cette approche mais avoir'list' object has no attribute 'complex_filter'
grillazz
466

Essaye ça:

matches = pages | articles | posts

Il conserve toutes les fonctions des ensembles de requêtes, ce qui est bien si vous le souhaitez order_byou similaire.

Remarque: cela ne fonctionne pas sur les ensembles de requêtes de deux modèles différents.

Daniel Holmes
la source
10
Ne fonctionne pas sur les ensembles de requêtes en tranches, cependant. Ou est-ce que je manque quelque chose?
sthzg
1
J'avais l'habitude de joindre les ensembles de requêtes en utilisant "|" mais ne fonctionne pas toujours bien. Il vaut mieux utiliser "Q": docs.djangoproject.com/en/dev/topics/db/queries/…
Ignacio Pérez
1
Il ne semble pas créer de doublons, en utilisant Django 1.6.
Teekin
15
Voici |l'opérateur d'union défini, pas OR au niveau du bit.
e100
6
@ e100 non, ce n'est pas l'opérateur d'union défini. django surcharge l'opérateur OR au niveau du bit: github.com/django/django/blob/master/django/db/models/…
shangxiao
109

Associé, 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, une qs.union()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 QuerySets. 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 l'argument all = True.

union (), intersection () et difference () renvoient des instances de modèle du type du premier QuerySet même si les arguments sont des QuerySets d'autres modèles. La transmission de différents modèles fonctionne tant que la liste SELECT est la même dans tous les QuerySets (au moins les types, les noms n'ont pas d'importance tant que les types dans le même ordre).

De plus, seuls LIMIT, OFFSET et ORDER BY (c'est-à-dire le découpage et order_by ()) sont autorisés sur le QuerySet résultant. En outre, les bases de données imposent des restrictions sur les opérations autorisées dans les requêtes combinées. 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/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union

Udi
la source
Il s'agit d'une meilleure solution pour mon ensemble de problèmes qui doit avoir des valeurs uniques.
Burning Crystals
Ne fonctionne pas pour les géométries geodjango.
MarMat
D'où importez-vous l'union? Doit-il provenir de l'un des X ensembles de requêtes?
Jack
Oui, c'est une méthode de jeu de requêtes.
Udi
Je pense que cela supprime les filtres de recherche
Pierre Cordier
76

Vous pouvez utiliser la QuerySetChainclasse ci-dessous. Lorsque vous l'utilisez avec le paginateur de Django, il ne devrait frapper la base de données qu'avec des COUNT(*)requêtes pour tous les ensembles de SELECT()requêtes et des requêtes uniquement pour les ensembles de requêtes dont les enregistrements sont affichés sur la page actuelle.

Notez que vous devez spécifier template_name=si vous utilisez un QuerySetChainavec 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 matchesavec le paginateur comme vous l'avez utilisé result_listdans votre exemple.

Le itertoolsmodule a été introduit dans Python 2.3, il devrait donc être disponible dans toutes les versions Python sur lesquelles Django fonctionne.

akaihola
la source
5
Belle approche, mais un problème que je vois ici est que les ensembles de requêtes sont ajoutés "tête-bêche". Que faire si chaque ensemble de requêtes est ordonné par date et que l'on a besoin que l'ensemble combiné soit également ordonné par date?
hasen
Cela semble certes prometteur, super, je vais devoir essayer ça, mais je n'ai pas le temps aujourd'hui. Je vous répondrai si cela résout mon problème. Bon travail.
espenhogbakk
Ok, j'ai dû essayer aujourd'hui, mais cela n'a pas fonctionné, d'abord il s'est plaint qu'il n'avait pas à attribuer _clone donc j'ai ajouté celui-là, j'ai juste copié le _all et cela a fonctionné, mais il semble que le paginateur ait un problème avec cet ensemble de requêtes. J'obtiens cette erreur de pagination: "len () de l'objet non dimensionné"
espenhogbakk
1
@Espen Bibliothèque Python: pdb, journalisation. Externe: IPython, ipdb, django-logging, django-debug-toolbar, django-command-extensions, werkzeug. Utilisez des instructions d'impression dans le code ou utilisez le module de journalisation. Surtout, apprenez à vous introspecter dans la coquille. Google pour les articles de blog sur le débogage de Django. Heureux de vous aider!
akaihola
4
@patrick voir djangosnippets.org/snippets/1103 et djangosnippets.org/snippets/1933 - surtout ce dernier est une solution très complète
akaihola
27

Le gros inconvénient de votre approche actuelle est son inefficacité avec de grands ensembles de résultats de recherche, car vous devez à chaque fois retirer l'ensemble de résultats de la base de données, même si vous ne souhaitez afficher qu'une 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 QuerySet, pas une liste. Si vous faites cela, Django coupe réellement le QuerySet avant que la requête ne soit exécutée, donc la requête SQL utilisera OFFSET et LIMIT pour obtenir uniquement les enregistrements que vous afficherez réellement. Mais vous ne pouvez pas le faire à moins que vous ne puissiez recadrer votre recherche en une seule requête.

Étant donné que les trois de vos modèles ont des champs de titre et de corps, pourquoi ne pas utiliser l' héritage de modèle ? Faites hériter les trois modèles d'un ancêtre commun qui a un titre et un corps, et effectuez la recherche en une seule requête sur le modèle ancêtre.

Carl Meyer
la source
23

Si vous souhaitez chaîner un grand nombre d'ensembles de requêtes, essayez ceci:

from itertools import chain
result = list(chain(*docs))

où: docs est une liste d'ensembles de requêtes

vutran
la source
8

Cela peut être réalisé de deux manières.

1ère façon de le faire

Utilisez l'opérateur 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 les ensembles de requêtes à l'aide de l'opérateur d'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 le faire

Une autre façon de réaliser une opération de combinaison entre deux ensembles de requêtes consiste à utiliser la fonction de chaîne itertools .

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
Devang Padhiyar
la source
7

Exigences: Django==2.0.2 ,django-querysetsequence==0.8

Dans le cas où vous souhaitez combiner querysetset toujours sortir avec un QuerySet, vous voudrez peut-être vérifier django-queryset-sequence .

Mais une note à ce sujet. Il n'en faut que deux querysetscomme argument. Mais avec python, reducevous pouvez toujours l'appliquer à plusieurs querysets.

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 travaillé list comprehension, reduceetdjango-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})
chidimo
la source
1
Ne fait Book.objects.filter(owner__mentor=mentor)pas la même chose? Je ne suis pas sûr que ce soit un cas d'utilisation valide. Je pense qu'un Bookpeut avoir besoin d'avoir plusieurs owners avant de commencer à faire quelque chose comme ça.
Will S
Oui, ça fait la même chose. Je l'ai essayé. Quoi qu'il en soit, cela pourrait peut-être être utile dans une autre situation. Merci d'avoir fait remarquer cela. Vous ne commencez pas exactement à connaître tous les raccourcis en tant que débutant. Parfois, il faut parcourir la route sinueuse pour apprécier la mouche du corbeau
chidimo
6

voici une idée ... il suffit de tirer vers le bas une page complète des résultats de chacun des trois, puis de jeter les 20 moins utiles ... cela élimine les grands ensembles de requêtes et de cette façon vous ne sacrifiez qu'un peu de performances au lieu de beaucoup

Jiaaro
la source
1

Cela fera le travail sans utiliser d'autres bibliothèques

result_list = list(page_list) + list(article_list) + list(post_list)
Satyam Faujdar
la source
-1

Cette fonction récursive concatène un tableau d'ensembles de requêtes en un seul ensemble de requêtes.

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
Petr Dvořáček
la source
1
Je suis littéralement perdu.
lycuide
nous combinant le résultat de la requête, il ne peut pas être utilisé au moment de l'exécution et cette très mauvaise idée de le faire. parce que parfois cela ajoute une duplication au résultat.
Devang Hingu