Pregunta ¿Cómo combinar 2 o más querysets en una vista de Django?


Estoy intentando construir la búsqueda de un sitio de Django que estoy construyendo, y en la búsqueda estoy buscando en 3 modelos diferentes. Y para obtener la paginación en la lista de resultados de búsqueda, me gustaría utilizar una vista genérica object_list para mostrar los resultados. Pero para hacer eso tengo que fusionar 3 conjuntos de consultas en uno.

¿Cómo puedo hacer eso? He intentado esto:

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

Pero esto no funciona Recibo un error cuando trato de usar esa lista en la vista genérica. A la lista le falta el atributo de clonación.

Alguien sabe cómo puedo fusionar las tres listas, page_list, article_list y post_list?


520
2018-01-10 19:51


origen


Respuestas:


Concatenar los conjuntos de consultas en una lista es el enfoque más simple. Si la base de datos se aplicará a todos los conjuntos de consultas de todos modos (por ejemplo, porque el resultado debe ser ordenado), esto no agregará un costo adicional.

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

Utilizando itertools.chain es más rápido que hacer un bucle en cada lista y agregar elementos uno por uno, ya que itertools se implementa en C. También consume menos memoria que convertir cada conjunto de consulta en una lista antes de concatenar.

Ahora es posible ordenar la lista resultante, p. por fecha (como se solicitó en el comentario de hasen j a otra respuesta). los sorted() la función acepta convenientemente un generador y devuelve una lista:

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

Si usa Python 2.4 o posterior, puede usar attrgetter en lugar de una lambda Recuerdo haber leído que era más rápido, pero no vi una diferencia de velocidad notable para una lista de millones de elementos.

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

886
2018-01-12 08:00



Prueba esto:

matches = pages | articles | posts

Conserva todas las funciones de los querysets, lo que es bueno si quieres order_by o similar.

Vaya, tenga en cuenta que esto no funciona en los conjuntos de consulta de dos modelos diferentes ...


387
2018-04-28 05:48



Puedes usar el QuerySetChain clase a continuación. Al usarlo con el paginador de Django, solo debería golpear la base de datos con COUNT(*) consultas para todos los conjuntos de consulta y SELECT() consultas solo para aquellos querysets cuyos registros se muestran en la página actual.

Tenga en cuenta que debe especificar template_name= si usa una QuerySetChain con vistas genéricas, incluso si los conjuntos de consultas encadenados usan el mismo 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()

En su ejemplo, el uso sería:

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)

Entonces usa matches con el paginador como usaste result_list en tu ejemplo.

los itertools El módulo se introdujo en Python 2.3, por lo que debería estar disponible en todas las versiones de Python en las que se ejecuta Django.


67
2018-01-11 09:51



Relacionado, para mezclar querysets del mismo modelo, o para campos similares de algunos modelos, Comenzando con Django 1.11 un qs.union() método también está disponible:

union()

union(*other_qs, all=False)

Nuevo en Django 1.11. Utiliza el operador UNION de SQL para combinar los resultados de dos o más QuerySets. Por ejemplo:

>>> qs1.union(qs2, qs3)

El operador de UNION selecciona solo valores distintos por defecto. Para permitir valores duplicados, use all = True   argumento.

union (), intersection () y difference () devuelven ejemplos de instancias   el tipo del primer QuerySet incluso si los argumentos son QuerySets de   Otros modelos. Pasar diferentes modelos funciona siempre que SELECCIONE   la lista es la misma en todos los QuerySets (al menos los tipos, los nombres no   importa siempre y cuando los tipos estén en el mismo orden).

Además, solo LIMIT, OFFSET y ORDER BY (es decir, cortar y   order_by ()) están permitidos en el QuerySet resultante. Promover, bases de   colocar restricciones sobre qué operaciones están permitidas en el combinado   consultas. Por ejemplo, la mayoría de las bases de datos no permiten LIMIT o OFFSET en   las consultas combinadas.

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


62
2018-02-12 11:23



La gran desventaja de su enfoque actual es su ineficacia con grandes conjuntos de resultados de búsqueda, ya que tiene que desplegar todo el conjunto de resultados de la base de datos cada vez, aunque solo tenga la intención de mostrar una página de resultados.

Para poder desplegar solo los objetos que realmente necesita de la base de datos, debe usar la paginación en un QuerySet, no en una lista. Si hace esto, Django realmente corta el QuerySet antes de que se ejecute la consulta, por lo que la consulta SQL usará OFFSET y LIMIT para obtener solo los registros que realmente mostrará. Pero no puede hacer esto a menos que pueda meter su búsqueda en una sola consulta de alguna manera.

Dado que los tres modelos tienen campos de título y cuerpo, ¿por qué no usarlos? modelo de herencia? Simplemente haga que los tres modelos hereden de un ancestro común que tenga título y cuerpo, y realice la búsqueda como una única consulta en el modelo antepasado.


24
2018-01-10 22:43



En caso de que quiera encadenar una gran cantidad de conjuntos de consulta, intente esto:

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

donde: docs es una lista de conjuntos de consulta


18
2017-11-26 21:42



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 de https://groups.google.com/forum/#!topic/django-users/6wUNuJa4jVw. Ver Alex Gaynor 


13
2017-12-23 12:42



Parece que t_rybik ha creado una solución integral en http://www.djangosnippets.org/snippets/1933/


8
2018-03-21 18:17



Para buscar es mejor usar soluciones dedicadas como Almiar - es muy flexible.


8
2018-04-09 08:52



he aquí una idea ... simplemente despliegue una página completa de resultados de cada uno de los tres y luego elimine los 20 menos útiles ... esto elimina los grandes conjuntos de consulta y de esa manera solo sacrifica un poco de rendimiento en lugar de un montón


4
2018-01-13 17:12