4

In my Django project I need to provide a view to get random object from a model using django-rest-framework. I had this ListAPIView:

class RandomObject(generics.ListAPIView):
   queryset = MyModel.objects.all().order_by('?')[:1]
   serializer_class = MyModelSerializer
   ...

It worked fine but order_by('?') takes a lot of time when launched on big database. So I decided to use usual Python random.

import random

def pick_random_object():
   return random.randrange(1, MyModel.objects.all().count() + 1)

class RandomObject(generics.ListAPIView):
   queryset = MyModel.objects.all().filter(id = pick_random_object())
   ...

I found out a strange thing when tried to use this. I launched Django development server and sent some GET requests, but I got absolutely the same object for all of the requests. When dev server restarted and another set of requests sent I'm getting another object, but still absolutely the same one for all of requests - even if random.seed() was used first of all. Meanwhile, when I tried to get a random object not via REST but via python manage.py shell I got random objects for every time I called pick_random_object().

So everything looks good when using shell and the behavior is strange when using REST, and I have no clue of what's wrong.

Everything was executed on Django development server (python manage.py runserver).

4
  • Don't know a huge amount about python/django, but this issue implies that the same seed is being used for the random number generator. What argument are you passing to random.seed? Commented Dec 11, 2013 at 14:54
  • @Slicedpan I pass no argument actually. As I understand, time seed is used as default when nothing sent. For the shell everything works acceptable even without seed() so I don't think the glitch concentrated in it. Commented Dec 11, 2013 at 14:57
  • yeah thats what the docs say. your code above seems to assume that the ids for the objects are consecutive and are numbered from 1, are you sure that's always correct Commented Dec 11, 2013 at 15:02
  • @Slicedpan Well, yes. At least it is correct for all the cases I tested this stuff on. Commented Dec 11, 2013 at 15:07

2 Answers 2

4

As @CarltonGibson noticed, queryset is an attribute of RandomObject class. Hence it cached and cannot be changed any later. So if you want to make some changeable queryset (like getting random objects at every request) in some APIView, you must override a get_queryset() method. So instead of

class RandomObject(generics.ListAPIView):
   queryset = MyModel.objects.all().filter(id = pick_random_object())
   ...

you should write something like this:

class RandomObject(generics.ListAPIView):
   #queryset = MyModel.objects.all().filter(id = pick_random_object())
   def get_queryset(self):
      return MyModel.objects.all().filter(id = pick_random_object())

Here pick_random_object() is a method to get random id from the model.

Sign up to request clarification or add additional context in comments.

Comments

2

Since it's an attribute of the class, your queryset is getting evaluated and cached when the class is loaded, i.e. when you start the dev server.

I'd try pulling a list of primary keys, using values_list() — the flat=True example does exactly what you need. Ideally cache that. Pick a primary key at random and then use that to get() the actual object when you need it.

So, how would that go?

I'd define a method on the view. If you forget the caching, the implementation might go like this:

# Lets use this...
from random import choice

def random_MyModel(self):
    """Method of RandomObject to pick random MyModel"""
    pks =  MyModel.objects.values_list('pk', flat=True).order_by('id')
    random_pk = choice(pks)
    return MyModel.objects.get(pk=random_pk)

You might then want to cache the first look up here. The caching docs linked above explain how to do that. If you do cache the result look into the db.models signals to know when to invalidate — I guess you'd post_save, checking the created flag, and post_delete.

I hope that helps.

2 Comments

I think you could not to edit the part concerning caching of querysets because I believe that is the problem in my case. And I didn't really get the idea about values_list(). Unfortunately, due to django-rest-framework API I should keep queryset in the same way as I described. I have to think of some other queryset or some way of sending a queryset or even some way to avoid caching in this particular case. And yes, that helped a lot, I didn't know about the caching.
Ok I re-added the bit on queryset caching and expanded my alternative suggestion. With luck that gives you enough to be going on with.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.