views:

1134

answers:

5

I have a model that represents paintings I present on my site. On the main webpage I'd like to show some of them: newest, one that was not visited for most time, most popular one and a random one.

I'm using Django 1.0.2.

While first 3 of them are easy to pull using django models, last one (random) causes me some trouble. I can ofc code it in my view, to something like this:

number_of_records = models.Painting.objects.count()
random_index = int(random.random()*number_of_records)+1
random_paint = models.Painting.get(pk = random_index)

It doesn't look like something I'd like to have in my view tho - this is entirely part of database abstraction and should be in the model. Also, here I need to take care of removed records (then number of all records won't cover me all the possible key values) and probably lots of other things.

Any other options how I can do it, preferably somehow inside the model abstraction?

+2  A: 

You could create a manager on your model to do this sort of thing. To first understand what a manager is, the Painting.objects method is a manager that contains all(), filter(), get(), etc. Creating your own manager allows you to pre-filter results and have all these same methods, as well as your own custom methods, work on the results.

EDIT: I modified my code to reflect the order_by['?'] method. Note that the manager returns an unlimited number of random models. Because of this I've included a bit of usage code to show how to get just a single model.

from django.db import models

class RandomManager(models.Manager):
    def get_query_set(self):
        return super(RandomManager, self).get_query_set().order_by('?')

class Painting(models.Model):
    title = models.CharField(max_length=100)
    author = models.CharField(max_length=50)

    objects = models.Manager() # The default manager.
    randoms = RandomManager() # The random-specific manager.

Usage

random_painting = Painting.randoms.all()[0]

Lastly, you can have many managers on your models, so feel free to create a LeastViewsManager() or MostPopularManager().

Soviut
Using get() would only work if your pks are consecutive, ie you never delete any items. Otherwise you are likely to try and get a pk that doesn't exist. Using .all()[random_index] doesn't suffer from this problem and isn't any less efficient.
Daniel Roseman
I understood that which is why my example simply replicates the question's code with a manager. It will still be up to the OP to work out his bounds checking.
Soviut
instead of using .get(id=random_index) wouldnt it be better to use .filter(id__gte=random_index)[0:1] ? First, it helps solving the problem with non-consecutive pks. Second, get_query_set should return... a QuerySet. And in your example, it does not.
NicDumZ
I wouldn't create a new manager just to house one method. I'd add "get_random" to the default manager so that you wouldn't have to go through the all()[0] hoop everytime you need the random image. Furthermore, if author were a ForeignKey to a User model, you could say user.painting_set.get_random().
Antti Rasinen
I typically create a new manager when I want a blanket action, like getting a list of random records. I'd create a method on the default manager if i were doing a more specific task with the records i already had.
Soviut
+12  A: 

Simply use:

MyModel.objects.order_by('?')[0]

It is documented in QuerySet API.

muhuk
Please note that this approach can be very slow, as documented :)
NicDumZ
"may be expensive and slow, depending on the database backend you're using." - any experience on diffrent DB backends? (sqlite/mysql/postgres)?
kender
I haven't tested it, so this is pure speculation: why should it be slower than retrieving all items and performing randomization in Python?
muhuk
Think this approach _can_ be slow, but doesn't have to. If DB backend doesn't support it this will be done in Python. If it does, the DB returns random record
kender
i read that it's slow in mysql, as mysql has incredibly inefficient random ordering.
Brandon H
If the backend doesn't support random ordering it makes sense for this to be slow as the ORM has to retrieve all records, shuffle them (likely O(n)) and then take the first one.
friism
please see http://buffis.com/2008/01/20/how-to-get-random-rows-from-mysql-using-django/comment-page-1/
chefsmart
+2  A: 

You could simply do:

models.Painting.objects.all().order_by('?')[:1].get()

which is much more efficient.

Arnaud
+7  A: 

The solutions with order_by('?')[:N] are extremely slow even for medium-sized tables if you use MySQL (don't know about other databases).

order_by('?')[:N] will be translated to SELECT ... FROM ... WHERE ... ORDER BY RAND() LIMIT N query.

It means that for every row in table the RAND() function will be executed, then the whole table will be sorted according to value of this function and then first N records will be returned. If your tables are small, this is fine. But in most cases this is a very slow query.

I wrote simple function that works even if id's have holes (some rows where deleted):

def get_random_item(model, max_id=None):
    if max_id is None:
        max_id = model.objects.aggregate(Max('id')).values()[0]
    min_id = math.ceil(max_id*random.random())
    return model.objects.filter(id__gte=min_id)[0]

It is faster than order_by('?') in almost all cases.

Mike Korobov
+1  A: 

Using order_by('?') will kill the db server on the second day in production. A better way is something like what is described here: Getting a random row from a relational database.

class PaintingManager(models.Manager):
    def random(self):
        count = self.aggregate(count=Count('id'))['count']
        random_index = randint(0, count - 1)
        return self.all()[random_index]
Emil Ivanov