800 votes

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

J'essaie de construire la recherche pour un site Django que je suis en train de construire, et dans cette recherche, je cherche dans trois modèles différents. Et pour obtenir la pagination sur la liste des résultats de la recherche, je voudrais utiliser une vue générique object_list pour afficher les résultats. Mais pour ce faire, je dois fusionner trois ensembles de requêtes 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 j'essaie d'utiliser cette liste dans la vue générique. Il manque l'attribut clone à la liste.

Comment puis-je fusionner les trois listes, page_list , article_list y post_list ?

1 votes

Il semble que t_rybik ait créé une solution complète à l'adresse suivante djangosnippets.org/snippets/1933

1 votes

Pour la recherche, il est préférable d'utiliser des solutions dédiées telles que Haystack - il est très flexible.

1 votes

Utilisateurs de Django 1.11 et abv, voyez cette réponse - stackoverflow.com/a/42186970/6003362

1213voto

akaihola Points 10007

Concaténer les querysets en une liste est l'approche la plus simple. Si la base de données est de toute façon interrogée pour tous les ensembles de requêtes (par exemple, parce que le résultat doit être trié), cela n'ajoute pas de coût supplémentaire.

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

Utilisation de itertools.chain est plus rapide que de boucler chaque liste et d'ajouter les éléments un par un, car itertools est implémenté en C. Il consomme également moins de mémoire que de convertir chaque queryset en liste avant de le concaténer.

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). Le site 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 une version ultérieure, vous pouvez utiliser attrgetter au lieu 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 une liste d'un million d'éléments.

from operator import attrgetter
result_list = sorted(
    chain(page_list, article_list, post_list),
    key=attrgetter('date_created'))

20 votes

Si vous fusionnez des ensembles de requêtes provenant de la même table pour effectuer une requête OR, et que vous avez des lignes dupliquées, 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)]

2 votes

Ok, alors nm à propos de la fonction groupby dans ce contexte. Avec la fonction Q, vous devriez être en mesure d'effectuer n'importe quelle requête OR dont vous avez besoin : https://docs.djangoproject.com/en/1.3/topics/db/queries/#complex-lookups-with-q-objects

0 votes

Est-il possible d'utiliser list.extend au lieu de chain ?

561voto

Essayez ça :

matches = pages | articles | posts

Il conserve toutes les fonctions des querysets, ce qui est intéressant si vous voulez order_by ou similaire.

Veuillez noter : cela ne fonctionne pas sur les querysets de deux modèles différents.

14 votes

Ne fonctionne pas sur les querysets tranchés, cependant. Ou est-ce que quelque chose m'échappe ?

0 votes

| semble s'apparenter davantage à une liste + qu'à un ensemble |, c'est-à-dire qu'il peut créer des doublons.

2 votes

J'avais l'habitude de joindre les querysets en utilisant "|" mais cela ne fonctionne pas toujours. Il est préférable d'utiliser "Q" : docs.djangoproject.com/fr/dev/topics/db/queries/

163voto

Udi Points 6298

Connexes, pour mélanger des ensembles de querelles provenant du même modèle, ou pour des champs similaires provenant de quelques modèles, démarrage con Django 1.11 a QuerySet.union() méthode est également disponible :

union()

union(*other_qs, all=False)

Nouveautés de Django 1.11 . Utilise l'opérateur UNION de SQL pour combiner les résultats de deux ou plusieurs ensembles 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 dupliquées, utilisez l'argument all=True l'argument all=True.

union(), intersection() et différence() renvoient des instances de modèle de type type du premier QuerySet, même si les arguments sont des QuerySets d'autres modèles. d'autres modèles. Le passage 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 les QuerySets sont les mêmes). les noms n'ont pas d'importance tant que les types sont dans le même ordre).

De plus, seuls LIMIT, OFFSET et ORDER BY (c'est-à-dire slicing et order_by()) sont autorisés sur le QuerySet résultant. Plus loin, bases de données imposent des restrictions sur les opérations qui sont 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 les requêtes combinées.

1 votes

C'est une meilleure solution pour mon ensemble de problèmes qui doit avoir des valeurs uniques.

1 votes

Mais d'où importez-vous le syndicat ? Doit-elle provenir d'un des X ensembles de requêtes ?

1 votes

Oui, c'est une méthode de queryset.

83voto

akaihola Points 10007

Vous pouvez utiliser le QuerySetChain ci-dessous. Lorsqu'elle est utilisée avec le paginateur de Django, elle ne doit accéder à la base de données qu'avec la commande COUNT(*) des requêtes pour tous les ensembles de requêtes et SELECT() uniquement pour les requêtes dont les enregistrements sont affichés sur la page actuelle.

Notez que vous devez spécifier template_name= si vous utilisez un QuerySetChain avec des vues génériques, même si les querysets enchaî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'usage 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 l'avez utilisé result_list dans votre exemple.

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

7 votes

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 se passe-t-il si chaque jeu de requêtes est ordonné par date et que l'on a besoin que le jeu combiné soit également ordonné par date ?

0 votes

Cela semble certainement prometteur, génial, je vais devoir essayer, mais je n'ai pas le temps aujourd'hui. Je reviendrai vous voir si cela résout mon problème. Excellent travail.

0 votes

Ok, j'ai dû essayer aujourd'hui, mais ça n'a pas marché, d'abord il s'est plaint qu'il n'y avait pas d'attribut _clone alors j'ai ajouté celui-ci, j'ai juste copié le _all et ça a marché, mais il semble que le paginateur ait un problème avec ce queryset. J'obtiens cette erreur du paginateur : "len() d'un objet non dimensionné".

41voto

vutran Points 456

Si vous voulez enchaîner un grand nombre de querysets, essayez ceci :

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

où : docs est une liste de querysets

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X