Django Generic Keys And Relations

dboost.me
2 min readJan 28, 2023

--

Let’s take a look at a simplest GenericForeignKey and GenericRelation usage. Imagine we are facebook and we show ads: with single image and single video.

Since we are already smart enough, we are going to have 3 models to store the banner content:

  • Ad general model with advertises, text and impressions count
  • PhotoBanner model for photo content
  • VideoBanner model for video content
class VideoBanner(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
video_url = models.URLField()
thumbnail_url = models.URLField()

ad = GenericRelation(
"Ad", "banner_id", "banner_type", related_query_name="video"
)


class PhotoBanner(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
photo_url = models.URLField()

ad = GenericRelation(
"Ad", "banner_id", "banner_type", related_query_name="photo"
)


class Ad(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)

advertiser = models.TextField()
text = models.TextField()
link_url = models.URLField()

impressions = models.IntegerField(default=0)
clicks = models.IntegerField(default=0)

banner_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)
banner_id = models.UUIDField(null=True)

banner = GenericForeignKey('banner_type', 'banner_id')

General model will have two stored fields:

  • banner_type reference to a target content table
  • banner_id reference to an id of target content in it’s table

Django tracks each model you create in ContentType table. That’s why banner type is a foreign key to that table.

class ContentType(models.Model):
app_label = models.CharField(max_length=100)
model = models.CharField(_("python model class name"), max_length=100)

Handy things we can do now:

ad = Ad.objects.first()


# will be either PhotoBanner or VideoBanner
banner = ad.banner


# get photo banners with 'photo' in url
photo_ads = Ad.objects.filter(photo__photo_url__contains="photo")


# get video banners with 'video' in url
photo_ads = Ad.objects.filter(video__video_url__contains="video")


# will return 3 most used content urls (either video or photo)
result = Ad.objects.values(
"photo__photo_url", "video__video_url"
).annotate(ads_count=Count("*")).order_by("-ads_count")[:3]

In this toy sample for sure you can store both references in the main table. But imagine you are implementing an automation engine for your home with hundreds of actions:

  • open a garage
  • report temperature
  • send sms
  • turn light on
  • turn music on
  • call 911

Of course we may add a column for each type of action eventually leading to a table with 100 columns, where each row will have a single value set. In that case using generic keys is a bless:

class Action(models.Model):
open_garage = models.ForeignKey("OpenGarage")
report_temperature = models.ForeignKey("ReportTemperature")
send_sms = models.ForeignKey("SendSms")
turn_light_on = models.ForeignKey("LightsOn")
music_on = models.ForeignKey("MusicOn")
call_911 = models.ForeignKey("Call911")


class NiceAction(models.Model):
action_type = models.ForeignKey(ContentType, on_delete=models.DO_NOTHING)
action_id = models.UUIDField(null=True)

Use generic keys with heart. Until next time!

--

--

No responses yet