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 countPhotoBanner
model for photo contentVideoBanner
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 tablebanner_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!