Milano
Milano

Reputation: 18705

Get next and previous object from QuerySet by object

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?

Upvotes: 2

Views: 4150

Answers (4)

Dolev Pearl
Dolev Pearl

Reputation: 284

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:

Upvotes: 5

albar
albar

Reputation: 3080

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...

Upvotes: 0

Fomalhaut
Fomalhaut

Reputation: 9727

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.

Upvotes: 0

Marcell Erasmus
Marcell Erasmus

Reputation: 914

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

Upvotes: 2

Related Questions