skip to content

Content Types & Generic Relations

The contenttypes framework: the ContentType registry, GenericForeignKey with GenericRelation, and GenericPrefetch. Interviewers probe why a generic link has no database constraint and joins poorly.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Django, how would you model a Like that can point at a Photo, a Comment or a Post using GenericForeignKey?

level: middleimportance: must knowfreq 50%

answer

  1. three parts on the Like model
  2. ForeignKey to ContentType
  3. object_id holds the target key
  4. reverse side needs its own field
  5. index the pair yourself

basics

~20 s

Give Like a ForeignKey to ContentType, an object_id column holding the target's primary key, and a GenericForeignKey combining the two; add GenericRelation(Like) on Photo, Comment and Post for reverse access, and add the index yourself.

solid answer

~40 s

Like gets three parts: `content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)`, `object_id = models.PositiveBigIntegerField()` and `content_object = GenericForeignKey('content_type', 'object_id')`, those two names being the defaults. The `GenericForeignKey` has no column of its own: assigning `like.content_object = photo` fills both columns, and reading it loads the target. For the reverse side, put `likes = GenericRelation(Like)` on each target, which gives `photo.likes.all()` and `photo.likes.create(user=u)` and makes deleting a photo delete its likes. Django does not index the pair automatically, so add `models.Index(fields=['content_type', 'object_id'])`, and a `UniqueConstraint` on user, content type and object id to block double likes. `object_id` must be able to hold every target's primary key type.

code

python · 31 lines
python
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models


class Like(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
    content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
    object_id = models.PositiveBigIntegerField()
    content_object = GenericForeignKey('content_type', 'object_id')
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        indexes = [models.Index(fields=['content_type', 'object_id'])]
        constraints = [
            models.UniqueConstraint(
                fields=['user', 'content_type', 'object_id'],
                name='one_like_per_user_per_object',
            ),
        ]


class Photo(models.Model):
    title = models.CharField(max_length=200)
    likes = GenericRelation(Like)


class Comment(models.Model):
    body = models.TextField()
    likes = GenericRelation(Like)

go deeper

for a junior

Know the three pieces on the Like model and that content_object is assigned like a normal foreign key.

for a middle

Explain what GenericRelation adds on the target side, why the composite index must be declared by hand, and how object_id's type must match the targets.

for a senior

Show the production details: the unique constraint against double likes, the separate lookup index, and which targets cascade their likes on delete.

for a principal

Frame when a single generic Like table is worth its lost constraints compared with per-target tables, given how many feature teams will query it.

## The problem a generic link solves A like, a tag, a comment or an audit entry often has to attach to **several different models**. An ordinary `ForeignKey` points at exactly one model, so a Like with one `ForeignKey` could only like photos. Django's contenttypes framework offers a **generic relation**: store *which model* in one column and *which row* in another, and let a virtual attribute stitch them together. ## The three parts on the Like model 1. A `ForeignKey` to `ContentType`, conventionally named `content_type`. This is a real foreign key with a real constraint, pointing at the registry table `django_content_type`. 2. A column that stores the target's primary key, conventionally `object_id`. For integer keys the docs suggest `PositiveBigIntegerField`, which matches `BigAutoField`, the project default for `DEFAULT_AUTO_FIELD` since Django 6.0. 3. A `GenericForeignKey`, conventionally `content_object`, given the names of the two fields. If they are called `content_type` and `object_id`, the arguments can be omitted. The `GenericForeignKey` is a **private field with no database column**. Assigning an object sets `content_type` to that object's `ContentType` and `object_id` to its primary key; nothing is written until `save()`. Reading it looks up the content type (from the cache) and runs one query for the target. You can also pass it to the constructor: `Like.objects.create(user=u, content_object=photo)`. ## Reverse access with GenericRelation The target models know nothing about Like until you add a `GenericRelation`: - `likes = GenericRelation(Like)` on `Photo`, `Comment` and `Post` adds no column; it describes the join `like.object_id = photo.id AND like.content_type_id = <Photo's id>`. - `photo.likes` is a related manager: `.all()`, `.count()`, `.create(user=u)`, `.filter(user=u).exists()`. - Deleting a `Photo` makes Django's deletion collector delete the likes pointing at it, because `GenericRelation` behaves like a cascade. - The reverse field also lets you filter and aggregate from the target side, for example `Photo.objects.annotate(n=Count('likes'))`. If the field names on Like are not the defaults, pass `content_type_field` and `object_id_field` to `GenericRelation` as well. ## Indexes and constraints Django will not add for you A plain `ForeignKey` gets an index automatically; a `GenericForeignKey` does **not**. Every `photo.likes` query and every cascade filters on (`content_type`, `object_id`), so declare that composite index in `Meta.indexes`. To stop a user liking the same object twice, add a `UniqueConstraint` on (`user`, `content_type`, `object_id`). That constraint's index starts with `user`, so it does not serve lookups by target; keep both. ## Choosing the object_id type | Target primary keys | object_id field | Trade-off | |---|---|---| | all `BigAutoField` | `PositiveBigIntegerField` | compact, fast comparisons | | all `UUIDField` | `UUIDField` | fine while every target uses UUIDs | | a mix of integers and strings | `CharField` | values are coerced to strings; wider index | | unknown or unbounded | `TextField` | maximum flexibility; the docs warn of performance penalties on some backends | The rule from the docs: the target's primary key values must be coercible to the `object_id` field's type by its `get_db_prep_value()`. ## What you get and what you do not - You **get** one table for all likes, attach-anywhere flexibility and a manager on every target. - You **do not get** a database foreign-key constraint on `object_id`, an `on_delete` option on the `GenericForeignKey`, `filter(content_object=...)`, `select_related('content_object')`, or a form field in a `ModelForm`. - Deleting a target cascades only when that target declares a `GenericRelation`; otherwise the likes stay behind and `content_object` returns `None`. ## Common mistakes in interviews and code reviews - **Forgetting the reverse field.** Teams add `GenericRelation` to Photo but not to Comment, then wonder why deleted comments leave likes behind. Every target that should cascade needs its own `GenericRelation`. - **Mismatched field names.** If Like uses `target_type` and `target_id`, both the `GenericForeignKey` and every `GenericRelation` must name them; the system check `contenttypes.E004` fires when a `GenericRelation` points at a model that has no matching `GenericForeignKey`. - **Expecting form support.** A `GenericForeignKey` never appears in a `ModelForm`; views set `content_object` in code, usually from a URL that names the target. - **Treating the virtual field as data.** Serializers and exports must write `content_type` (ideally as a natural key) and `object_id`, because `content_object` itself is not stored anywhere. A useful way to describe the design in an interview: the `ContentType` foreign key answers *which table*, `object_id` answers *which row*, and the `GenericForeignKey` is a convenience accessor that the database never sees.

  • What changes if Post uses a UUID primary key while Photo and Comment use BigAutoField?
    A UUID cannot be stored in `PositiveBigIntegerField`, so `object_id` has to become a type every key coerces into, usually `CharField` or `TextField`. Integers are then stored as strings, the index is wider, and comparisons are string comparisons; the docs warn that `TextField` may carry significant performance penalties on some backends. Often the cleaner fix is to keep key types uniform across the models a generic link can target.
  • How do you stop the same user liking the same photo twice?
    Add `UniqueConstraint(fields=['user', 'content_type', 'object_id'], name=...)` to `Like.Meta.constraints`, so the database rejects the duplicate even under concurrent requests. In the view, catch `IntegrityError` or use `get_or_create(user=u, content_type=ct, object_id=photo.pk)` on the concrete columns so a double click returns the existing like instead of a 500.

saying these in an interview costs you the question

  • content_object is a real column you can index directly
  • GenericForeignKey takes on_delete just like ForeignKey
  • Django indexes content_type and object_id automatically
  • GenericRelation adds a column to the Photo table
  • object_id can use any type whatever the targets' primary keys are
open as a page

In Django, what is the ContentType model, and what does ContentType.objects.get_for_model() give you?

level: juniorimportance: should knowfreq 30%

basics

~20 s

ContentType is the contenttypes app's registry: one row per installed model, identified by app_label and model name. get_for_model() returns that row for a model class or instance, creating it if missing and caching it in-process.

open as a page

In Django, why does a GenericForeignKey get no database foreign-key constraint, and when would you replace it with concrete ForeignKeys?

level: seniorimportance: should knowfreq 42%

basics

~20 s

object_id can hold a key from any table, so the database cannot declare a constraint on it; integrity, cascades and joins move into Django. For a few fixed targets, nullable ForeignKeys with a CheckConstraint, or per-target tables, restore both.

open as a page

In Django, a feed shows 50 likes and reads like.content_object for each; how do prefetch_related and GenericPrefetch cut the query count?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Each content_object read runs its own query. prefetch_related('content_object') groups likes by content type and runs one query per target model; GenericPrefetch, new in Django 5.0, supplies a custom queryset per type, such as one using select_related or only().

open as a page