2

I have and object and QuerySet which contains this object. I need to get next and previous object of this QuerySet.

How can I do that?

I could get next this way:

next = False
for o in QuerySet:
    if next:
        return o
    if o==object:
       next = True

but I think it's very slow and unefficient approach on huge QuerySets.

Do you know a better solution?

Milano
  • 18,048
  • 37
  • 153
  • 353

4 Answers4

5

I know this question is a little bit old but I came across this and didn't find very performant solutions, so I hope it may help someone. There are 2 pretty good solutions I came up with.

The 1st is more elegant although slightly worse-performing. The 2nd is significantly faster, especially for larger QuerySets, but it combines using Raw SQL.

They both find previous & next ids, but can, of course, be tweaked to retrieve actual object instances.

1st solution:

object_ids = list(filtered_objects.values_list('id', flat=True))
current_pos = object_ids.index(my_object.id)
if current_pos < len(object_ids) - 1:
    next_id = object_ids[current_pos + 1]
if current_pos > 0:
    previous_id = object_ids[current_pos - 1]

2nd solution:

window = {'order_by': ordering_fields}
with_neighbor_objects = filtered_objects.annotate(
    next_id=Window(
        Lead('id'),
        **window
    ),
    previous_id=Window(
        Lag('id'),
        **window
    ),
)
sql, params = with_neighbor_objects.query.sql_with_params()
#  wrap the windowed query with another query using raw SQL, as
#  simply using .filter() will destroy the window, as the query itself will change.
current_object_with_neighbors = next(r for r in filtered_objects.raw(f"""
        SELECT id, previous_id, next_id FROM ({sql}) filtered_objects_table
        WHERE id=%s
    """, [*params, object_id]))

next_id = current_object_with_neighbors.next_id:
previous_id = current_object_with_neighbors.previous_id:
Dolev Pearl
  • 264
  • 2
  • 8
  • Great solution, thank you. I used the 1st and it works great. I added a wrap so that if you are at the end of the list, next_id = object_ids[0] and if you are at the beginning, previous_id = len(object_ids) - 1. – Mustafamond77 Aug 27 '22 at 13:17
2

Using the Django QuerySet API You can try the following:

For Next:

qs.filter(pk__gt=obj.pk).order_by('pk').first()

For previous:

qs.filter(pk__lt=obj.pk).order_by('-pk').first()

Marcell Erasmus
  • 866
  • 5
  • 15
  • Thanks Marcell but the base QuerySet doesn't have to be ordered by id. It can be ordered by anything. – Milano Aug 23 '18 at 15:45
0

Probably that is what you need (in Python 3, if you need a solution for Python 2.7, let me know):

def get_next(queryset, obj):
    it = iter(queryset)
    while obj is not next(it):
        pass
    try:
        return next(it)
    except StopIteraction:
        return None

def get_prev(queryset, obj):
    prev = None
    for o in queryset:
        if o is obj:
            break
        prev = obj
    return prev

But there are some notes:

  1. As far as full queryset is stored into the variable, you can keep an index of your object and extract next and previous ones as [i + 1] and [i - 1]. Otherwise you have to go through whole queryset to find your object there.
  2. According to PEP8 you shouldn't name variables like QuerySet, such names should be used for classes. Name it queryset.
Fomalhaut
  • 8,590
  • 8
  • 51
  • 95
0

Maybe you can use something like that:

def get_prev_next(obj, qset):
    assert obj in qset
    qset = list(qset)
    obj_index = qset.index(obj)
    try:
        previous = qset[obj_index-1]
    except IndexError:
        previous = None    
    try:
        next = qset[obj_index+1]
    except IndexError:
        next = None
    return previous,next

It's not very beautiful, but it should work...

albar
  • 3,020
  • 1
  • 14
  • 27