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.
Passing in a string for the namespace also gives you the ability to have multiple Ratings for a single model.
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.
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.