May 1, 2010 will

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.

Use Markdown for formatting
*Italic* **Bold** `inline code` Links to [Google](http://www.google.com) > This is a quote > ```python import this ```
your comment will be previewed here
gravatar
Simon
I like the method. But do you realy need the name space and db_table attribute? Would not the table name be generated form the class names (ArticleRating and ImageRating) and the app name there by avoiding conflicts?
gravatar
Will McGugan
Simon, that would be nicer, but at the time make_rating_model is called, the ArticleRating & ImageRating classes don't exist – so there's no way of automatically retrieving the class names.

Passing in a string for the namespace also gives you the ability to have multiple Ratings for a single model.
gravatar
Simon
The ability to have multiple Ratings for a single model is a nice extra that is not posible with my suggestion.

I don't think when make_rating_model is called is the critical time, as it is creating an abstract model that will be inhereted from. When django is creating the database table (syncdb) it will look at ArticleRating in your myapp/model.py and create the table name from that. The same is the case when you import ArticleRating from myapp/model.py and utilise it in the app.
gravatar
Harro
Ohh… code generation ;-)

I never thought to use it to generate an abstract base class and then extend it.. I like it.

I can think of some uses where you really want to split the different types.
I can see some uses for improving django's comments app to use this to allow for different comments. Recently I had to create a site with normal comments and comments with ratings. This would have worked there.
gravatar
paluh
I think that my friend Patrys has written similar solution, but he creates abstract classes in place. This solution allows you build inheritance structure containing generic models (for example: you can easily subclass ‘ProductFactory’ from his blog post code). It also replaces __module__ value, so when using it you don't have to pass ‘namespace’ when constructing model. I know it's in polish but I think code is self explanatory: http://room-303.com/blog/2010/04/27/django-abstrakcji-ciag-dalszy/