Como combinar 2 ou mais querysets em uma view do Django?

Eu estou tentando construir a busca por um site Django que estou construindo, e na pesquisa estou pesquisando em 3 modelos diferentes. E para obter paginação na lista de resultados da pesquisa, gostaria de usar uma exibição object_list genérica para exibir os resultados. Mas para fazer isso eu tenho que mesclar 3 querysets em um.

Como eu posso fazer isso? Eu tentei isso:

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") 

Mas isso não funciona eu recebo um erro quando tento usar essa lista na visão genérica. A lista está faltando o atributo clone.

Alguém sabe como eu posso mesclar as três listas, page_list , article_list e post_list ?

Concatenar os querysets em uma lista é a abordagem mais simples. Se o database for atingido para todos os querysets de qualquer maneira (por exemplo, porque o resultado precisa ser classificado), isso não aumentará os custos.

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

Usar itertools.chain é mais rápido do que fazer o loop de cada lista e append elementos um por um, já que itertools é implementado em C. Ele também consome menos memory do que convertendo cada queryset em uma lista antes de concatenar.

Agora é possível classificar a lista resultante, por exemplo, por data (conforme solicitado no comentário do hasen j para outra resposta). A function sorted() aceita convenientemente um gerador e retorna uma lista:

 result_list = sorted( chain(page_list, article_list, post_list), key=lambda instance: instance.date_created) 

Se você estiver usando o Python 2.4 ou posterior, você pode usar o attrgetter vez de um lambda. Lembro-me de ler sobre ser mais rápido, mas não vi uma diferença de velocidade notável para uma lista de milhões de itens.

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

Tente isto:

 matches = pages | articles | posts 

Mantém todas as funções dos querysets, o que é bom se você quiser order_by ou similar.

Opa, por favor note que isto não funciona em querysets de dois modelos diferentes …

Você pode usar a class QuerySetChain abaixo. Ao usá-lo com o paginador do Django, ele só deve atingir o database com consultas COUNT(*) para todas as querysets e consultas SELECT() somente para aqueles querysets cujos registros são exibidos na página atual.

Observe que você precisa especificar template_name= se estiver usando um QuerySetChain com visualizações genéricas, mesmo se todos os conjuntos de consultas encadeados usarem o mesmo modelo.

 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() 

No seu exemplo, o uso seria:

 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) 

Em seguida, use matches com o paginador como você usou result_list no seu exemplo.

O módulo itertools foi introduzido no Python 2.3, portanto deve estar disponível em todas as versões do Python em que o Django é executado.

Relacionado, para misturar querysets do mesmo modelo, ou para campos similares de alguns modelos, o Começando com Django 1.11 um método qs.union() também está disponível:

union()

 union(*other_qs, all=False) 

Novo no Django 1.11 . Usa o operador UNION do SQL para combinar os resultados de dois ou mais QuerySets. Por exemplo:

 >>> qs1.union(qs2, qs3) 

O operador UNION seleciona apenas valores distintos por padrão. Para permitir valores duplicados, use o argumento all = True.

union (), intersection () e difference () retornam instâncias de modelo do tipo do primeiro QuerySet, mesmo se os argumentos forem QuerySets de outros modelos. Passar modelos diferentes funciona desde que a lista SELECT seja a mesma em todos os QuerySets (pelo menos os tipos, os nomes não importam, desde que os tipos estejam na mesma ordem).

Além disso, somente LIMIT, OFFSET e ORDER BY (ou seja, slicing e order_by ()) são permitidos no QuerySet resultante. Além disso, os bancos de dados impõem restrições sobre quais operações são permitidas nas consultas combinadas. Por exemplo, a maioria dos bancos de dados não permite LIMIT ou OFFSET nas consultas combinadas.

https://docs.djangoproject.com/en/1.11/ref/models/querysets/#django.db.models.query.QuerySet.union

A grande desvantagem da sua abordagem atual é sua ineficiência com grandes conjuntos de resultados de pesquisa, já que você precisa extrair todo o conjunto de resultados do database a cada vez, mesmo que você pretenda exibir apenas uma página de resultados.

Para apenas baixar os objects que você realmente precisa do database, você precisa usar a paginação em um QuerySet, não em uma lista. Se você fizer isso, o Django corta o QuerySet antes que a consulta seja executada, portanto, a consulta SQL usará OFFSET e LIMIT para obter apenas os registros que você realmente exibirá. Mas você não pode fazer isso a menos que você possa colocar sua pesquisa em uma única consulta de alguma forma.

Dado que todos os seus três modelos têm campos de título e corpo, por que não usar inheritance de modelos ? Basta ter todos os três modelos herdados de um ancestral comum que tenha título e corpo e realizar a pesquisa como uma única consulta no modelo ancestral.

Caso você deseje encadear muitas consultas, tente o seguinte:

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

onde: docs é uma lista de querysets

 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) 

Citado em https://groups.google.com/forum/#!topic/django-users/6wUNuJa4jVw . Veja Alex Gaynor

Parece que o t_rybik criou uma solução abrangente em http://www.djangosnippets.org/snippets/1933/

Para pesquisar, é melhor usar soluções dedicadas como o Haystack – é muito flexível.

aqui está uma idéia … basta puxar para baixo uma página inteira de resultados de cada um dos três e então jogar fora os 20 menos úteis … isso elimina os grandes querysets e dessa forma você só sacrifica um pouco de performance ao invés de muito

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

Caso você queira combinar querysets e ainda sair com um QuerySet , você pode querer dar uma olhada no django-queryset-sequence .

Mas uma nota sobre isso. Leva apenas dois querysets como argumento. Mas com o python reduce você pode sempre aplicá-lo a vários queryset s.

 from functools import reduce from queryset_sequence import QuerySetSequence combined_queryset = reduce(QuerySetSequence, list_of_queryset) 

E é isso. Abaixo está uma situação que eu corri e como eu usei list comprehension , reduce e 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})