web-dev-qa-db-fra.com

Comment combiner 2 ou plusieurs ensembles de requêtes 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 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 jeux 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 est manquant dans la liste.

Tout le monde sait comment je peux fusionner les trois listes, page_list, article_list et post_list?

603
espenhogbakk

La concaténation des ensembles de requêtes dans une liste est l'approche la plus simple. Si, malgré tout, 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 en liste avant la concaténation.

Maintenant, il est 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 sorted() 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 version ultérieure, vous pouvez utiliser attrgetter au lieu d'un lambda. Je me souviens avoir lu que c'était plus rapide, mais je ne voyais pas de différence de vitesse notable pour une liste d'articles comportant un million d'éléments.

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

Essaye ça:

matches = pages | articles | posts

Il conserve toutes les fonctions des requêtes, ce qui est bien si vous voulez utiliser order_by ou similaire.

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

421
bryan

Dans le même ordre d'idées, pour mélanger des ensembles de requêtes du même modèle ou 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)

Nouveau dans 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 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. Passer 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’importent pas tant que les types sont dans le même ordre).

De plus, seuls LIMIT, OFFSET et ORDER BY (c.-à-d. Slicing et order_by ()) sont autorisés sur le QuerySet résultant. De plus, les bases de données imposent des restrictions quant aux 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/fr/1.11/ref/models/querysets/#Django.db.models.query.QuerySet.union

95
Udi

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 les requêtes COUNT(*) pour tous les ensembles de requêtes et les requêtes SELECT() uniquement pour les ensembles 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 la pagination 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 _ Django.

75
akaihola

L’inconvénient majeur 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 n’avez l’intention que d’afficher 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 ensemble de requêtes, pas une liste. Si vous faites cela, Django coupe en fait le QuerySet avant que la requête ne soit exécutée. La requête SQL utilisera donc OFFSET et LIMIT pour obtenir uniquement 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 titre et corps, pourquoi ne pas utiliser héritage du 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.

27
Carl Meyer

Si vous souhaitez enchaîner beaucoup de jeux de requêtes, essayez ceci:

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

où: docs est une liste de requêtes

21
vutran
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

16
ray6080

voici une idée ... il suffit de tirer une page complète des résultats de chacun des trois et ensuite de jeter les 20 moins utiles ... cela élimine les gros ensembles de requêtes et de cette façon vous ne faites que sacrifier un peu de performance au lieu de beaucoup

6
Jiaaro

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

1ère façon de faire cela

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 faire cela

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

from itertools import chain
combined_results = list(chain(pagelist1, pagelist2))
6
Devang Padhiyar

Conditions requises: Django==2.0.2, Django-querysetsequence==0.8

Si vous voulez combiner querysets et que vous ayez toujours un QuerySet, vous pouvez vérifier Django-queryset-sequence .

Mais une note à ce sujet. Il ne faut que deux querysets comme argument. Mais avec python reduce, vous 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 utilisé 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})
6
chidimo

Cette fonction récursive concatène un tableau 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
1
Petr Dvořáček