The Missing Manager for Django Models with GFK Relationships
Save yourself writing the same queries time and time again by creating a simple model manager with common queries for your generic foreign keys
Generic relations are a powerful piece of the Django toolkit that allow you to create model-agnostic foreign key relationships with other parts of your Django project. If you aren’t familiar with how they work, have a read through the ContentType
documentation as their functionality is outside the scope of this post.
The only downside is that they require a little bit of boilerplate code to get up and running and you sometimes find yourself writing the same code numerous times, particularly if you are using GFKs heavily in your application. To make things DRYer, let’s outsource some of this boilerplate code.
First let’s look at an example. A simple GFK relationship might be used to create a voting application that allows you to easily attach a positive or negative vote to a variety of different content:
class Vote(models.Model):
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
author = models.ForeignKey("auth.User")
value = models.IntegerField() # +1 or -1
#...
class Comment(models.Model):
author = models.ForeignKey("auth.User")
content = models.TextField()
#...
class Review(models.Model):
author = models.ForeignKey("auth.User")
content = models.TextField()
#...
Django provides a GenericForeignKey
model field that makes it easy to traverse the relationship from one side to the other (in this case from a Vote
to a Comment
or Review
and visa versa). It also provides some useful methods on the ContentType
model that allow us to switch easily between ContentType
model instances and the actual class objects those instance represent.
Even with these helpers, there are three very common queries I find myself doing again and again with GFKs:
- get all vote objects related to a particular user (i.e. “get all of John’s votes”)
- get all vote objects related to particular model (i.e. “get all the votes in our review sections”)
- get all vote objects for a particular instance (i.e. “get all the votes for review X”)
We should write a model manager to take care of these for us!
The first thing we can do is to move the GFK relationship itself into an abstract model that can be inherited:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.generic import GenericForeignKey
class GKFModel(models.Model):
"""
Adds the required fields for a model to implement a
GFK relationship and also adds a simple 'create'
method that can be used to create the relationship.
"""
content_type = models.ForeignKey(ContentType)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
class Meta:
abstract = True
class Vote(GFKModel):
author = models.ForeignKey("auth.User")
value = models.IntegerField() # +1 or -1
#...
Then we can create a simple model manager that provides three common queries for objects implementing a GFK relationship:
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import SafeText
from django.db.models.loading import get_model
class GFKManager(models.Manager):
user_field = "author"
def for_user(self, user, queryset=None):
if not queryset:
queryset = self.get_queryset()
return queryset.filter(**{self.user_field: user})
def for_model(self, model, queryset=None):
"""
Returns a queryset that will fetch all instances that
are related to a paricular model (for example all Votes
on Comment objects) The default queryset is used but can
be replaced using the "queryset" kwarg (for example if
you wanted to add "prefetch_related" or "select_related"
"""
if isinstance(model, (str, SafeText)):
model = get_model(*model.rsplit('.', 1))
ct = ContentType.objects.get_for_model(model)
if not queryset:
queryset = self.get_queryset()
return queryset.filter(content_type=ct)
def for_object(self, obj, queryset=None):
"""
Returns a queryset that will fetch all instance that are
related to a particular instance (for example all
Votes on a single particular Comment object). Like
above,the default queryset
"""
return self.for_model(obj.__class__, queryset=queryset).filter(object_id=obj.pk)
class Vote(models.Model):
objects = GFKManager()
#...
Vote.objects.for_model(Comment)
Vote.objects.for_object(Comment.objects.get(pk=1))
The beauty of this is that these can be reused for all models that want to implement a GFK relationship. In a larger project that might have numerous models that implement GFK relationships (commenting, liking, voting, flagging content etc.) this can save us a lot of repetitive code and make our views elsewhere much cleaner.