Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to chain Django querysets preserving individual order

I'd like to append or chain several Querysets in Django, preserving the order of each one (not the result). I'm using a third-party library to paginate the result, and it only accepts lists or querysets. I've tried these options:

Queryset join: Doesn't preserve ordering in individual querysets, so I can't use this.

result = queryset_1 | queryset_2

Using itertools: Calling list() on the chain object actually evaluates the querysets and this could cause a lot of overhead. Doesn't it?

result = list(itertools.chain(queryset_1, queryset_2))

How do you think I should go?

like image 662
Caumons Avatar asked Aug 14 '13 15:08

Caumons


3 Answers

This solution prevents duplicates:

q1 = Q(...)
q2 = Q(...)
q3 = Q(...)
qs = (
    Model.objects
    .filter(q1 | q2 | q3)
    .annotate(
        search_type_ordering=Case(
            When(q1, then=Value(2)),
            When(q2, then=Value(1)),
            When(q3, then=Value(0)),
            default=Value(-1),
            output_field=IntegerField(),
        )
    )
    .order_by('-search_type_ordering', ...)
)
like image 158
Yurii Liubchenko Avatar answered Oct 20 '22 03:10

Yurii Liubchenko


If the querysets are of different models, you have to evaluate them to lists and then you can just append:

result = list(queryset_1) + list(queryset_2)

If they are the same model, you should combine the queries using the Q object and 'order_by("queryset_1 field", "queryset_2 field")'.

The right answer largely depends on why you want to combine these and how you are going to use the results.

like image 9
Demiurge Avatar answered Oct 20 '22 02:10

Demiurge


So, inspired by Peter's answer this is what I did in my project (Django 2.2):

from django.db import models
from .models import MyModel

# Add an extra field to each query with a constant value
queryset_0 = MyModel.objects.annotate(
    qs_order=models.Value(0, models.IntegerField())
)

# Each constant should basically act as the position where we want the 
# queryset to stay
queryset_1 = MyModel.objects.annotate(
    qs_order=models.Value(1, models.IntegerField()) 
)

[...]

queryset_n = MyModel.objects.annotate(
    qs_order=models.Value(n, models.IntegerField()) 
)

# Finally, I ordered the union result by that extra field.
union = queryset_0.union(
    queryset_1, 
    queryset_2, 
    [...], 
    queryset_n).order_by('qs_order')

With this, I could order the resulting union as I wanted without changing any private attribute while only evaluating the querysets once.

like image 5
Original BBQ Sauce Avatar answered Oct 20 '22 01:10

Original BBQ Sauce