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 ?

Était-ce utile?

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
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top