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?

528
demandé sur Bill Armstrong 2009-01-10 22:51:43

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'))
900
répondu akaihola 2009-01-12 08:00:46

Essayez ceci:

matches = pages | articles | posts

Conserve toutes les fonctions des querysets ce qui est bien si vous voulez order_by ou similaire.

Oups, veuillez noter que cela ne fonctionne pas sur les querysets de deux modèles différents...

393
répondu 2009-05-01 02:45:19

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.

67
répondu akaihola 2009-01-14 12:57:22

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

67
répondu Udi 2017-04-25 07:47:47

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.

24
répondu Carl Meyer 2009-01-10 22:43:25

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

18
répondu vutran 2012-11-26 21:42:21
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

13
répondu ray6080 2013-12-23 12:42:00

On dirait que t_rybik a créé une solution complète à http://www.djangosnippets.org/snippets/1933/

8
répondu akaihola 2010-03-21 18:17:21

Pour la recherche, il est préférable d'utiliser des solutions dédiées comme botte de Foin - c'est très souple.

8
répondu minder 2010-04-09 08:52:07

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

4
répondu Jiaaro 2009-01-13 17:12:01

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 querysets.

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})
4
répondu Parousia 2018-07-23 17:48:27