Damn foreign keys, stealing our jobs and women

Django has support for Generic Foreign Keys, which let you reference one model instance from another, without knowing up-front what that model type is. The classic use for something like this is for a commenting system; you need generic foreign keys – or something like them – because you wouldn’t want a commenting system that only worked with a single model.

If you have ever used generic foreign keys in Django, you will know that it is not quite transparent to the developer; a little effort is required to manage the various content types. I’ll present here an alternative method to achieve this late binding of foreign keys that doesn’t require storing the type of the object (as generic foreign keys do) and is completely transparent to the developer. I’m sure I’m not the first to think of this method, but I haven’t yet seen it used in other Django projects.

Rather than store the type of object in a separate field, we can create a new model for each foreign key type we want to reference. For example; lets say we have a Rating model, and we want to rate Articles and Images – we could do this by generating a ArticlesRating model and a ImagesRating model with appropriate foreign keys. The easiest way to do this is with a function that returns a parameterized class definition.

Here’s a snippet of code from a project I’m working on, that does just that:

rating.py

from django.db.models import Model, ForeignKey, IntegerField, Count, Avg
from django.db import IntegrityError
from django.contrib.auth.models import User

def make_rating_model(rated_model, namespace):

    class Rating(Model):

        user = ForeignKey(User)
        rated_object = ForeignKey(rated_model)
        vote = IntegerField(default=0, blank=True, null=False)

        class Meta:
            abstract=True
            db_table = u'rating_%s_%s' % (namespace, unicode(rated_model).lower())
            unique_together = ('user', 'rated_object')

        def __unicode__(self):
            return u"%s's rating of %s" % (self.user.username, unicode(self.rated_object))

        # Rest of the methods snipped for brevity
        # Contact me if you would like the whole class

    return Rating

This isn’t a model definition, rather it is a function that create a model definition. You can call it multiple times to return a Rating model for each object you want a rating for. The function, make_rating_model takes two parameters; the name of the model you want to rate, and a string that is used to generate the table name, to avoid naming conflicts.

To create a rating object you would import ratings in your models.py file and add the following:

class ArticleRating(ratings.make_rating_model('Article', 'mysite')):
    pass

class ImageRating(ratings.make_rating_model('Image', 'mysite')):
    pass

Now if you syncdb you will get two completely independent models with essentially the same interface – which means you can write code that works equally well with model instances of either type.

This method doesn’t quite replace generic foreign keys; if you don’t know until runtime what model to reference, or if you require the objects to be in a single table, then you will still need generic foreign keys, but in my experience this is rarely the case.

Read full article at “Django posts in ‘It’s All Geek to Me’”

Leave a comment