Generic Foreign Keys in Django
June 5, 2019, 8:11 a.m.
| Written By: Tanner
Black Cat

Sometimes you are creating a model with a foreign key, but you want to be flexible. For example, let's look at an example where you are creating a website for a music store and you have a bunch of albums. The albums could be performed/written by a single artist, or by a band. Let's say you have the following models:

class Album(models.Model):
    name = models.CharField(max_length = 50)
    performer = #what do we put here?

class Artist(models.Model):
    name = models.CharField(max_length = 75)
    band = models.ForeignKey(Band, on_delete = models.CASCADE, blank = True, null = True)

class Band(models.Model):
    name = models.CharField(max_length = 50)
    genre = models.CharField(max_length = 50)
    year_established = models.IntegerField()

DISCLAIMER: I AM NOT CLAIMING THAT THIS STRUCTURE IS THE ONLY WAY OR EVEN THE BEST WAY TO SET UP A DATABASE IN THIS CONTEXT. IT IS JUST AN EXAMPLE.

Now that we have gotten that part out of the way, we can look at what we have. We have an album that has a name, and we want to have a foreign key type relationship so that we know who the person is that performed the album. This could be a single person of type Artist or an instance of Band. We could create two nullable fields that will hold the Artist or Band in each, like this:

class Album(models.Model):
    name = models.CharField(max_length = 50)
    performer_artist = models.ForeignKey(Artist, on_delete = models.CASCADE, blank = True, null = True)
    performer_band = models.ForeignKey(Band, on_delete = models.CASCADE, blank = True, null = True)

The idea is that one of them is empty. But then we would need to add additional validators to make sure one of them is not null and when we refer to the performer we would need to check both fields. I smell a headache. So, we are going to find a way to make one performer field where it can be an Artist or Band. Throughout this example, we are going to want to add two albums to our database - AC/DC's Thunderstruck and Eminem's The Eminem Show, assuming we have a Band instance of AC/DC and an Artist instance of Eminem.


Generic Foreign Keys

With Generic Foreign Keys (GFKs), we can have a foreign key relationship with any type of model we want, and only have one relationship in our model. This does not come without some sort of cost, however. GFKs are made up of the following parts:

Content Type

The content type refers to the type of model itself. We need to tell the Generic Foreign Key which model type we are pointing to. In our example, we can make a field called content_type that will end up holding the model, which will end up being Artist or Band.

Object ID

We will need to create some sort of field that will point to our model instance. The content_type will only refer to our actual model class. However, we need to be able to tell our field which instance of the model class we are pointing to. By using an id, we know it can refer to nearly any model class because virtually every model will have an id. This field will be fittingly called object_id.

Content Object

This field will combine the content_type and the object_id to store the instance we are pointing to. This is using a field GenericForeignKey, and the field will be called content_object.


Putting it together

Let's add this to our code.

We can change our Album model to the following:

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models

class Album(models.Model):
    name = models.CharField(max_length = 50)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')

The content_type is a little funny. The way it is structured is by pointing to another model, ContentType. Don't be confused by this. ContentType is simply another model provided by Django that has a bunch of instances of your model types in your project. If you are confused by this, I suggest going into your Django shell (python manage.py shell) and querying ContentType.objects.all() to see what this does.

Next, we are going to and the object_id. This will hold the id of the object we are pointing to. Since it is just a number, we will make it a models.PositiveIntegerField().

Lastly, we need to define the actual object. We imported the GenericForeignKey field to our project, so we can use this to define the Foreign Key Relationship. This will take the 'content_type' and the 'object_id' to let our GenericForeignKey what type of model and the instance of the model we are pointing to.

So, let's try to add our two Albums. Let's assume we already have a Band instance of AC/DC and an Artist instance of Eminem. We can now add our performer to our Albums via our content_object field. We can do this in our shell (python manage.py shell).

>>> from albums.models import Album
>>> from artists.models import Artist
>>> from bands.models import Band
>>> acdc = Band.objects.get(name = "AC/DC")
>>> eminem = Artist.objects.get(name = "Eminem")
>>> thunderstruck = Album.objects.create(name = "Thunderstruck", content_object = acdc)
>>> eminem_show = Album.objects.create(name = "The Eminem Show", content_object = eminem)

Now, we have a our two new Albums by AC/DC and Eminem. We can now get the performer via the album.content_object property.

Generic Foreign Keys are great because we can refer to any type of model we want. However, be careful so we don't overuse it.

cat