ContentTypes & Generic Relations
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06), Python
3.12-3.14 and PostgreSQL 15 or newer, all checked on 2026-10-03; where the 5.2 LTS (5.2.17) behaves differently the text
says so. Every example was run on that stack in a scratch project on SQLite with the Django test runner, manage.py
check, dumpdata and remove_stale_contenttypes; none of them needs PostgreSQL, and the two places where PostgreSQL
would behave differently are called out. The contenttypes framework (django.contrib.contenttypes) keeps one database
row per installed model and lets a row of one table point at a row of any other table, which is what the official
documentation calls a generic (or polymorphic) relation. It is the machinery under the permission system and the admin
history, and it is the usual answer to "one comment, tag or attachment model shared by many kinds of object". The
Bookshelf application (the catalog app built across this section) uses it to tag books, authors and shelves with a
single TaggedItem table, and the second half of the page explains why that convenience has a price and when a plain
foreign key is the better design.
In a hurry: what goes where
| Need | Rule of thumb | Section |
|---|---|---|
Know which model a row refers to |
|
|
Point one row at any model |
A |
|
Reach the tags from a book |
Add |
|
Filter tags by a property of the target |
|
|
Avoid one query per row |
|
|
Edit generic objects in a form or the admin |
|
|
Move data between databases |
Dump with |
|
Decide whether to use any of it |
Prefer a real foreign key, several nullable keys with a constraint, or inheritance when the set of targets is small and known. |
The ContentType model
ContentType lives in django.contrib.contenttypes.models. Each row has two fields, app_label and model, which
together identify one installed model: the last component of the app’s import path (catalog, auth, sites) and the
lower-case model name (book, user). A read-only name property returns the model’s verbose_name. The framework
is part of the default INSTALLED_APPS that startproject writes; the admin (for its change history) and
django.contrib.auth (to tie permissions to models) both require it, so removing it is rarely an option.
Rows are created by the post_migrate signal: after every migrate, Django inserts a row for each model that has none,
including proxy models. ContentTypeManager.get_for_model() also creates the row on demand if it is missing.
>>> from django.contrib.contenttypes.models import ContentType
>>> ct = ContentType.objects.get(app_label="catalog", model="book")
>>> ct
<ContentType: Catalog | book>
>>> ct.name
'book'
>>> ct.model_class()
<class 'catalog.models.Book'>
>>> ct.get_object_for_this_type(title="The Dispossessed")
<Book: The Dispossessed>
model_class() returns the model class a row stands for (or None when the model no longer exists, see Fixtures, natural keys, renames and stale rows),
and get_object_for_this_type(using=None, **kwargs) runs a get() on that model. Together they allow code that works on
"some model, named at runtime" without importing it. The string form includes the app label (for example Catalog |
book) since Django 3.0, which disambiguates models with the same name in different apps.
ContentTypeManager and its cache
Do not look rows up with ContentType.objects.get(…) in application code. The custom manager has methods that share a
per-process cache, so after the first lookup there is no query at all:
from django.contrib.contenttypes.models import ContentType
from catalog.models import Author, Book, BookProxy, Shelf
book_ct = ContentType.objects.get_for_model(Book) # a class ...
same = ContentType.objects.get_for_model(Book.objects.first()) # ... or an instance
by_id = ContentType.objects.get_for_id(book_ct.pk) # shares the cache
mapping = ContentType.objects.get_for_models(Book, Author, Shelf)
# {<class 'catalog.models.Book'>: <ContentType: Catalog | book>, ...}
natural = ContentType.objects.get_by_natural_key("catalog", "book")
-
get_for_model(model, for_concrete_model=True)accepts a class or an instance. Withfor_concrete_model=Falseit returns the row of a proxy model instead of the row of the model the proxy is based on. -
get_for_models(*models, for_concrete_models=True)returns a dictionary and fetches all missing rows in one query, which is the right call when a view needs several types. -
get_for_id(id)shares the cache and is preferred overget(pk=id). -
get_by_natural_key(app_label, model)exists so that rows can be referenced by natural key when data is deserialized (see Fixtures, natural keys, renames and stale rows). -
clear_cache()empties the cache. Application code rarely needs it, but tests that count queries or reset state do.
from django.contrib.contenttypes.models import ContentType
from django.test import TestCase
class ContentTypesTests(TestCase):
def setUp(self):
ContentType.objects.clear_cache()
self.addCleanup(ContentType.objects.clear_cache)
Run in the scratch project, the first get_for_model(Book) issued one query and the next get_for_model() and
get_for_id() calls issued none.
Source: The ContentTypeManager
Proxy models
A proxy model shares its table with its concrete parent but gets its own ContentType row (it has its own permissions,
for example). get_for_model() returns the parent’s row by default; pass for_concrete_model=False to get the proxy’s:
class BookProxy(Book):
class Meta:
proxy = True
ContentType.objects.get_for_model(BookProxy) # the Book row
ContentType.objects.get_for_model(BookProxy, for_concrete_model=False) # the bookproxy row
GenericForeignKey has the same switch as a constructor argument, for_concrete_model=False, so that a generic key can
point at a proxy model’s row. Both behaviours were confirmed in the scratch tests.
Where Django itself uses it
The simplest use of ContentType is a plain ForeignKey to it, as a way of tying rows to model classes. The permission
system does exactly that: auth.Permission has a content_type foreign key, which is how add_book is known to belong
to catalog.Book. In the scratch project, Permission.objects.get(codename="add_book").content_type equals
ContentType.objects.get_for_model(Book). See Permissions &
Authorization for the rest of that machinery. The admin’s LogEntry does the same to record which kind of object was
changed. Everything below goes one step further and adds the object’s primary key, which makes the relation generic.
Source: Methods on ContentType instances
GenericForeignKey
A normal ForeignKey points at exactly one model. A GenericForeignKey stores two plain columns instead (a content type
and an object id) and combines them in Python, so one field can point at a row of any model.
tag, content_type_id, object_id"] C["django_content_type
id, app_label, model"] B["catalog_book
id = 7"] S["catalog_shelf
id = a UUID"] T -->|"content_type_id (real foreign key)"| C T -.->|"object_id = 7 (no constraint)"| B T -.->|"object_id = UUID text (no constraint)"| S
The solid arrow is a real foreign key with a database constraint. The dotted arrows are only a convention: the database knows nothing about them, which is the source of both the flexibility and the pitfalls discussed below.
The three parts
Setting one up takes three declarations on the model that does the pointing, as in the reference’s tagging example:
-
a
ForeignKeytoContentType(conventionally namedcontent_type); -
a field for the target’s primary key (conventionally
object_id, usually aPositiveBigIntegerField); -
a
GenericForeignKey("content_type", "object_id"). When the two fields have the conventional names, the arguments may be omitted.
import uuid
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType
from django.db import models
class Author(models.Model):
name = models.CharField(max_length=100)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
tags = GenericRelation("TaggedItem", related_query_name="book")
def __str__(self):
return self.title
class Shelf(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
tags = GenericRelation("TaggedItem", related_query_name="shelf")
def __str__(self):
return self.name
class TaggedItem(models.Model):
tag = models.SlugField()
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.CharField(max_length=64)
content_object = GenericForeignKey("content_type", "object_id")
class Meta:
indexes = [models.Index(fields=["content_type", "object_id"])]
def __str__(self):
return self.tag
The Meta.indexes entry matters: unlike a ForeignKey, a GenericForeignKey does not create a database index, and
every lookup of the tags of one object filters on the pair (content_type, object_id). The official documentation
recommends adding the composite index yourself and notes that this behaviour may change in the future.
Source: Generic relations
Choosing the type of object_id
The reference uses PositiveBigIntegerField, which suits the default BigAutoField keys (the default for new projects
since Django 6.0, see Where the books are outdated, and what changed recently). The rule is that the target’s primary key values must be coercible to the type of
the object_id field by its get_db_prep_value() method; the field type does not have to match the primary key’s type.
That is why the model above uses a CharField: it can hold the integer key of a Book and the UUID key of a Shelf in
one column. A TextField has no maximum length but, according to the documentation, may cost significant performance on
some backends; there is no universally best choice, so decide from the models you expect to point at.
The CharField choice has a cost, found while running the examples: the generic key stores the text form of the key
('78ce402e-8ef2-4a8c-86b5-03dfcb7b23cb' for a UUID), while a UUID column on SQLite stores its hex form without dashes.
The manager shelf.tags.all() works, because it filters object_id using the same conversion; the reverse lookup
TaggedItem.objects.filter(shelf__name=…) joins object_id to the shelf’s primary key column directly, and in the
scratch run it returned no rows. On PostgreSQL, which this page did not run, comparing a varchar column with an
integer or UUID column in a join is a type mismatch, so the same caution applies. If you rely on reverse lookups through
related_query_name, keep the target’s key type and the object_id type the same (a PositiveBigIntegerField for
integer keys, a UUIDField for UUID keys), which in practice means one generic-relation table per key type.
Source: GenericForeignKey
Using the field
The field behaves like a foreign key for reading and assigning, including as a keyword argument to the constructor and
to create():
>>> from catalog.models import Author, Book, Shelf, TaggedItem
>>> book = Book.objects.get(title="The Dispossessed")
>>> tag = TaggedItem.objects.create(content_object=book, tag="scifi")
>>> tag.content_type.model_class()
<class 'catalog.models.Book'>
>>> tag.object_id
'1'
>>> tag.content_object
<Book: The Dispossessed>
>>> shelf = Shelf.objects.create(name="Favourites")
>>> TaggedItem.objects.create(content_object=shelf, tag="mine").content_object
<Shelf: Favourites>
Assigning content_object fills content_type and object_id; reading it fetches the content type (from the cache) and
then the target row, one query per distinct object. The result is cached on the instance, so the second read costs
nothing.
Source: Generic relations
What a generic key cannot do
Because the field is not a real column, four things behave differently from a ForeignKey, and all four were reproduced
in the scratch tests:
-
No filtering.
TaggedItem.objects.filter(content_object=book)and.get(content_object=book)raiseFieldError. Filter on the two real fields instead (see Querying generic relations). -
Not in model forms. A
GenericForeignKeyis not a form field. Listing it in aModelForm’s `fieldsraisesFieldError; a form for the model offers onlytag, and the pointing is set in code or through the generic formset (see Generic inlines in forms and the admin). -
No integrity. If the target row is deleted,
content_typeandobject_idkeep their values andcontent_objectreturnsNone. Nothing stops you storing anobject_idthat never existed. -
No
on_delete.GenericForeignKeyaccepts noon_deleteargument, so the cascade behaviour of the next section is all you get. The documentation points at thepre_deletesignal (see Signals) for custom behaviour.
>>> TaggedItem.objects.filter(content_object=book)
Traceback (most recent call last):
...
django.core.exceptions.FieldError: Field 'content_object' does not generate an automatic reverse relation and therefore cannot be used for reverse querying. If it is a GenericForeignKey, consider adding a GenericRelation.
>>> author = Author.objects.get(name="Ursula K. Le Guin")
>>> orphan = TaggedItem.objects.create(content_object=author, tag="author-tag")
>>> author.delete()
>>> TaggedItem.objects.get(pk=orphan.pk).content_object is None
True
The error message already names the remedy, which is the next section. The dangling row remains in the table until you
delete it yourself, because Author has no GenericRelation here to cascade the deletion.
Source: Generic relations
GenericRelation
GenericRelation is the reverse side: a field declared on a target model that names the generic-key model. It creates
no column. It exists so that the target gets a related manager, can be queried from the tag side, takes part in
aggregation, and (most importantly) deletes the tags when it is deleted.
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
tags = GenericRelation("TaggedItem", related_query_name="book")
If the generic-key model uses non-default field names, the relation must be told which they are, with
content_type_field and object_id_field:
tags = GenericRelation(
TaggedItem,
content_type_field="content_type_fk",
object_id_field="object_primary_key",
)
Source: Reverse generic relations
The related manager
A Book now has a tags manager that filters on its own content type and key. It supports all(), filter() and the
usual queryset methods, plus add(), create(), set(), remove() and clear():
>>> t1 = TaggedItem.objects.create(content_object=book, tag="django")
>>> t2 = TaggedItem.objects.create(content_object=book, tag="python")
>>> book.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: python>]>
>>> t3 = TaggedItem(tag="web")
>>> book.tags.add(t3, bulk=False) # t3 is saved with the book's type and key
>>> book.tags.create(tag="framework")
<TaggedItem: framework>
>>> book.tags.set([t1, t3]) # removes (deletes) the others
>>> book.tags.all()
<QuerySet [<TaggedItem: django>, <TaggedItem: web>]>
>>> book.tags.remove(t3) # a bulk delete, not a detach
>>> book.tags.clear()
>>> TaggedItem.objects.count()
0
remove() and clear() delete the tag rows (a bulk delete, as the reference says); they do not detach them as the
manager of a nullable ForeignKey would, because a tag with no target has no meaning. add() of an unsaved instance needs
bulk=False, as in the example, so that Django saves the instance itself. Each call matched the behaviour shown in the
reference; the final count() of zero was asserted in the scratch tests.
Source: Reverse generic relations
Deletion cascades through GenericRelation
If you delete an object that has a GenericRelation, the rows pointing at it through a GenericForeignKey are deleted
too. Deleting a Book deletes its TaggedItem rows; deleting an Author (which declares no GenericRelation in this
example) leaves orphans, as the previous section showed. This is the only deletion behaviour the framework offers: there
is no on_delete option, and the documentation says that if you want to avoid the cascade, do not declare the
GenericRelation and provide any alternative behaviour with a pre_delete signal receiver.
Two consequences follow. First, declare a GenericRelation on every model that can be a target, even if you never use the
manager, or you will leak orphan rows. Second, the delete is performed by Django’s collector in Python, so queries that
bypass it (a raw SQL DELETE, or a database-level cascade such as DB_CASCADE on some other relation) do not clean the
tags up.
Source: Reverse generic relations
Querying generic relations
filter(content_object=…) fails, but there are three supported ways to ask the question.
Through related_query_name. Declaring related_query_name="book" on the relation creates a lookup from the
tag to the book (book__title), and the field name tags already gives the lookup in the other direction. Both work in
filter(), exclude() and order_by():
>>> TaggedItem.objects.filter(book__title__contains="Django")
<QuerySet [<TaggedItem: django>]>
>>> Book.objects.filter(tags__tag="scifi")
<QuerySet [<Book: The Dispossessed>]>
The query behind the first line joins catalog_book on object_id and also restricts content_type_id to the book’s
row, so shelves with the same numeric key are not matched. Mind the key-type remark from Choosing the type of object_id for UUID targets.
By content type and key. Without related_query_name (or for a single object) filter on the two real fields:
book_type = ContentType.objects.get_for_model(Book)
TaggedItem.objects.filter(content_type=book_type, object_id=book.pk)
# for several objects, a queryset can stand in for the key list
books = Book.objects.filter(title__contains="Django")
TaggedItem.objects.filter(content_type=book_type, object_id__in=books)
Always include the content type in such a filter. object_id alone matches the same number in every table, and the
scratch tests created a Book and an Author with the same primary key, each with its own tag, to confirm that only the
content type keeps them apart.
Through aggregation. The aggregation API works with a GenericRelation: Book.objects.aggregate(Count("tags"))
returned {'tags__count': 4} for four tags across two books, and Book.objects.annotate(n=Count("tags")) ranks books by
tag count in one query. See
Aggregation, Expressions & Functions.
Avoiding one query per row
A list of generic objects is a classic N+1 trap: each tag.content_object is one query on whichever table the row points
at. With three tags on a book and three on a shelf, iterating TaggedItem.objects.all() and reading content_object
issued 1 + 6 queries in the scratch tests (the content types were already cached).
from django.contrib.contenttypes.prefetch import GenericPrefetch
# 1. prefetch_related: one query per referenced table
tags = TaggedItem.objects.prefetch_related("content_object")
[t.content_object for t in tags] # 1 + 2 queries (tags, books, shelves)
# 2. GenericPrefetch: you choose the queryset for each model
prefetch = GenericPrefetch(
"content_object",
[Book.objects.select_related("author"), Shelf.objects.only("name")],
)
tags = TaggedItem.objects.prefetch_related(prefetch)
[t.content_object for t in tags] # 3 queries, books already joined to authors
# 3. From the target side: prefetch the generic relation
books = Book.objects.prefetch_related("tags") # 2 queries for all books and all their tags
prefetch_related() supports GenericRelation (case 3) as it does any many-valued relation. For a
GenericForeignKey (cases 1 and 2) the number of queries depends on the data: one per referenced table, plus possibly
queries on the ContentType table for types not yet cached. GenericPrefetch(lookup, querysets, to_attr=None) takes a
list of querysets, one for each ContentType the rows can reference, and it is how to apply select_related(),
only() or filters to each model; it appeared in Django 5.0 together with plain support for prefetching a heterogeneous
GenericForeignKey. The counts above were asserted with assertNumQueries(); see
Query Optimization & Fetch Modes for the general techniques.
Source: GenericPrefetch and prefetch_related()
Generic inlines in forms and the admin
Because GenericForeignKey is not a form field, editing the tags of a book needs a dedicated formset class, in the way
inlineformset_factory() handles a regular foreign key.
generic_inlineformset_factory
django.contrib.contenttypes.forms provides generic_inlineformset_factory(), built on modelformset_factory(), and the
base class BaseGenericInlineFormSet. Its signature is
generic_inlineformset_factory(model, form=ModelForm, formset=BaseGenericInlineFormSet, ct_field="content_type",
fk_field="object_id", fields=None, exclude=None, extra=3, can_order=False, can_delete=True, max_num=None,
formfield_callback=None, validate_max=False, for_concrete_model=True, min_num=None, validate_min=False,
absolute_max=None, can_delete_extra=True). Pass ct_field and fk_field when your fields have non-default names, and
for_concrete_model=False to match a GenericForeignKey declared that way. The other arguments are the same as for the
model formset factories; list fields explicitly, as the example does, so that no column is exposed by accident.
from django.contrib.contenttypes.forms import generic_inlineformset_factory
from .models import TaggedItem
TagFormSet = generic_inlineformset_factory(
TaggedItem, fields=["tag"], extra=2, can_delete=True
)
from django.shortcuts import get_object_or_404, redirect, render
from .forms import TagFormSet
from .models import Book
def book_tags(request, pk):
book = get_object_or_404(Book, pk=pk)
if request.method == "POST":
formset = TagFormSet(request.POST, instance=book)
if formset.is_valid():
formset.save()
return redirect("book-tags", pk=book.pk)
else:
formset = TagFormSet(instance=book)
return render(request, "catalog/book_tags.html", {"book": book, "formset": formset})
<h1>Tags for {{ book }}</h1>
<form method="post">
{% csrf_token %}
{{ formset.management_form }}
{{ formset.as_div }}
<button type="submit">Save</button>
</form>
from django.urls import path
from . import views
urlpatterns = [path("books/<int:pk>/tags/", views.book_tags, name="book-tags")]
The view is the same as for any inline formset: the form needs {% csrf_token %} and the management form, and save()
sets content_type and object_id from instance. In the scratch test the GET page rendered with two empty forms, a
POST of two tags redirected, and the book ended with exactly those two tags. The form prefix is built from the model and
the two field names (catalog-taggeditem-content_type-object_id), so the management-form keys used by tests and
JavaScript have that prefix. The formset fields offered were tag, the hidden id and DELETE, never the pointing
columns. See Model Forms & Formsets for formsets in general; Django 3.2
added the absolute_max and can_delete_extra arguments to this factory, so a hostile POST cannot instantiate an
unbounded number of forms when absolute_max is set.
Source: Generic relation in forms
GenericTabularInline and GenericStackedInline
The admin equivalent lives in django.contrib.contenttypes.admin. GenericInlineModelAdmin extends InlineModelAdmin
with two attributes, ct_field (the name of the content-type foreign key, default content_type) and ct_fk_field (the
name of the integer field holding the id, default object_id); GenericTabularInline and GenericStackedInline are its
tabular and stacked layouts. Used on the parent’s admin, they behave like any other inline:
from django.contrib import admin
from django.contrib.contenttypes.admin import GenericTabularInline
from .models import Author, Book, Shelf, TaggedItem
class TaggedItemInline(GenericTabularInline):
model = TaggedItem
extra = 1
fields = ["tag"]
@admin.register(Book)
class BookAdmin(admin.ModelAdmin):
list_display = ["title", "author"]
inlines = [TaggedItemInline]
@admin.register(Shelf)
class ShelfAdmin(admin.ModelAdmin):
inlines = [TaggedItemInline]
admin.site.register(Author)
One inline class serves both parents, because the inline derives the content type from whichever object is being edited.
In the scratch test a superuser opened the Book change page, which contained the inline’s management form, and posted a
new tag, which was saved with the book’s content type and key. The usual inline options (extra, min_num, max_num,
fields, classes, can_delete) apply as for any inline; see
The Admin Site for those and
Admin Customization for custom inlines.
Fixtures, natural keys, renames and stale rows
The primary key of a ContentType row is an artefact of the database: it depends on the order in which models were
first migrated, so it differs between a developer’s database, staging and production. Data that stores content_type_id
therefore cannot be moved by plain primary keys.
Natural keys
ContentType supports natural keys: the pair (app_label, model). dumpdata --natural-foreign writes the content
type of a TaggedItem as a pair, and deserialization resolves it with get_by_natural_key(). The documentation says that
when you serialize a model with generic relations you should probably use natural keys:
python manage.py dumpdata catalog.taggeditem --natural-foreign --indent 2
[
{
"model": "catalog.taggeditem",
"pk": 1,
"fields": {
"tag": "n",
"content_type": ["catalog", "book"],
"object_id": "1"
}
}
]
(The output above is reformatted from the single line dumpdata printed in the scratch run.) The test deleted the tags,
deserialized the dump with serializers.deserialize("json", …) and saved the objects, and the restored tag’s
content_object was the original book. Remember that object_id is still an uninterpreted number: the dump is only
portable if the target rows keep their primary keys. See
Fixtures, Serialization & Initial Data for natural keys and
loaddata in general.
Source: Generic relations, dumpdata --natural-foreign and Natural keys
Renames and stale content types
When a model is deleted, its ContentType row stays behind, because post_migrate only adds rows. The row is stale:
model_class() returns None for it, and anything that depends on it (permissions, and any generic-key rows pointing at
it) still exists. The remove_stale_contenttypes command, available when the contenttypes app is installed, deletes them;
it first lists the objects that would be deleted along with them and asks for confirmation, and it accepts
--database and --include-stale-apps (which also removes rows belonging to apps no longer in INSTALLED_APPS, added in
Django 3.1).
python manage.py remove_stale_contenttypes
python manage.py remove_stale_contenttypes --include-stale-apps --database default
In the scratch test a hand-created row catalog | gone was removed by the command with interactive=False, which printed
Deleting stale content type 'catalog | gone'. Treat the command as destructive: deleting a content type cascades to every
Permission and every generic-key row that references it, so run it after a deploy that deleted models, and review the
list it prints. Renaming a model is handled by the framework: before a migration run, the contenttypes app inserts a
step after each RenameModel operation so that the existing row is renamed instead of a new one being created next to
the old one, which keeps permissions and generic keys attached. The framework implements this on its own and does not
document it as an API, so check your migration plan with
Migrations when renaming a model that is the target of generic keys.
Source: remove_stale_contenttypes and Django 3.1 release notes
System checks
Models that declare a GenericForeignKey or GenericRelation are covered by the contenttypes system checks, which run
with every manage.py check and migrate (see System Checks):
| Check | Meaning |
|---|---|
|
The |
|
The |
|
The content-type field is not a |
|
The content-type field is not a |
|
Model names must be at most 100 characters (the length of |
|
The content-type field uses a database-level |
The most common mistake is a typo in the field names. Adding this model to the scratch app produced, from
manage.py check:
class Broken(models.Model):
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
thing = GenericForeignKey("content_type", "oid")
SystemCheckError: System check identified some issues:
ERRORS:
catalog.Broken.thing: (contenttypes.E001) The GenericForeignKey object ID references the nonexistent field 'oid'.
System check identified 1 issue (0 silenced).
The scratch tests also produced E002 (a content-type name that does not exist), E003 (an IntegerField named
content_type), E004 (a foreign key to Author) and E006 (models.DB_CASCADE on the content type). The last one is
worth remembering: a database-level cascade is carried out by the database without Django’s collector running, and the
checks reject that option for the content-type field of a generic key, so the check rejects the combination.
Async code
content_object is a plain attribute that runs a query when first read, so reading it from async code raises
SynchronousOnlyOperation. The scratch run confirmed it. Use the async queryset API, or wrap the access:
from asgiref.sync import sync_to_async
from django.contrib.contenttypes.models import ContentType
from catalog.models import TaggedItem
async def target_of(tag_pk):
tag = await TaggedItem.objects.aget(pk=tag_pk)
# option 1: resolve it yourself with the async ORM
content_type = await ContentType.objects.aget(pk=tag.content_type_id)
model = content_type.model_class()
return await model.objects.aget(pk=tag.object_id)
# option 2: run the attribute access in a thread
# return await sync_to_async(lambda: tag.content_object)()
Both options returned the book in the scratch run. Iterating a GenericRelation manager works with async for
([t.tag async for t in book.tags.all()]), as for any queryset. See
Making Queries for the async ORM interface.
Source: Async safety and Generic relations
When to avoid generic relations
The documentation shows generic relations as a feature, but they trade away exactly the things a relational database is for. Count the costs before choosing one:
-
No referential integrity. The database cannot stop
object_idfrom naming a row that does not exist, and it cannot cascade. Everything depends onGenericRelationbeing declared on every target and on every delete going through the ORM. -
No joins by default. Without
related_query_name, the database cannot join tags to targets, so reporting queries ("every tag on a book by this author") need several queries or manualobject_id__insubqueries. -
Weak typing and indexing. The key column is one type for all targets (and is a
CharFieldif mixed key types are needed, which breaks the reverse join as shown in Choosing the type of object_id), and the composite index is yours to maintain. -
N+1 by nature. Reading the targets of many rows costs one query per referenced table at best, which is why this page needs a prefetching section.
-
Awkward for forms, serializers and APIs. Model forms do not offer the field and serializer libraries need custom code for it, so each layer needs custom code (see Generic inlines in forms and the admin).
-
Awkward for data migration. Primary keys of content types differ between databases, and orphan rows survive deletions that bypass the collector.
The usual alternatives, ordered from the simplest:
-
A plain
ForeignKey. If the relation has one target model (comments on books, reviews of books) use a normal key. The Bookshelf reviews in the Relationships page do exactly that. -
A few nullable foreign keys plus a constraint. When the set of targets is small and known (a note on a book or on an author), give the model one nullable key per target and a
CheckConstraintthat requires exactly one of them. The database enforces integrity, the ORM joins and filters normally, and forms work. The cost is one column per target and a migration when a target is added. -
Model inheritance. If the targets share a common parent (
PublicationwithBookandMagazinechildren), point at the parent with a normal key; see Model Inheritance & Meta API. -
One table per target, generated from an abstract base class (
BookTag,ShelfTag), when the volume is high and each tag is queried only with its target. -
A many-to-many with a
throughmodel, if the same tag must attach to many targets of one kind.
class Note(models.Model):
text = models.CharField(max_length=200)
book = models.ForeignKey(
Book, null=True, blank=True, on_delete=models.CASCADE, related_name="notes"
)
author = models.ForeignKey(
Author, null=True, blank=True, on_delete=models.CASCADE, related_name="notes"
)
class Meta:
constraints = [
models.CheckConstraint(
condition=(
models.Q(book__isnull=False, author__isnull=True)
| models.Q(book__isnull=True, author__isnull=False)
),
name="note_exactly_one_target",
),
]
def __str__(self):
return self.text
In the scratch tests a Note with only a book and one with only an author were saved, while a note with both targets, and
one with neither, raised IntegrityError from the database. book.notes.all() and Book.objects.filter(notes__text=…)
are ordinary joins. (The argument is named condition; the older check argument was deprecated in Django 5.1.) See
Constraints, Indexes & Composite Keys for the constraint API.
Source: CheckConstraint
Generic relations remain the right tool when the set of targets is open (a reusable app that must attach tags or comments to models it has never seen), when you control all deletes, and when the target count is small enough that one query per referenced table is acceptable. Inside one project with a known list of models, the constraint-based design is almost always the better one.
Where the books are outdated, and what changed recently
The books consulted for this section (listed on the section bibliography page) describe the contenttypes framework as of
Django 1.x and 2.x. The concepts (the ContentType row, the three-part generic key, GenericRelation, generic inlines)
are unchanged and still work as they did; the edges differ.
-
object_idtype. Older tutorials declare the id asPositiveIntegerField, which the current admin documentation still shows in its inline example. The contenttypes reference now usesPositiveBigIntegerField(the field was added in Django 3.1), which matches theBigAutoFielddefault primary keys that new projects get from Django 6.0 on. WithAutoFieldorBigAutoFieldkeys the old integer field works until the first id above its limit. -
The index is yours.
index_togetheris gone (removed in Django 5.1); declare the composite index withMeta.indexes, as the reference does, because a generic key does not index itself. -
Prefetching. Heterogeneous prefetching of a
GenericForeignKey(rows pointing at several different models) arrived in 5.0, withGenericPrefetchto control each model’s queryset; books describe only the plainprefetch_related()call. -
Stale rows.
remove_stale_contenttypesgained--include-stale-appsin 3.1, which matters for projects that dropped apps long ago. -
Formset limits. The
absolute_maxandcan_delete_extraformset arguments (3.2) are not in any of the books. -
Related-manager caching. Django 4.1 briefly cached the related managers of
GenericRelationon the model instance; the change was reverted in 4.1.2, so nothing needs to be done today. -
Async. None of the books covers async views; the generic key is a synchronous attribute (see Async code).
| Version | Change |
|---|---|
3.0 |
|
3.1 |
|
3.2 |
|
4.1 |
Related managers (including |
5.0 |
|
5.1 |
|
6.0 |
|
6.1 |
|
Source: Django 6.1 release notes
Checklist
-
Every target model of a
GenericForeignKeydeclares aGenericRelation, so deletes cascade and no orphans are left. -
The generic-key model has a composite index on (
content_type,object_id), declared inMeta.indexes. -
object_idis aPositiveBigIntegerFieldunless the targets have mixed key types; withCharField, reverserelated_query_namejoins were checked on the target database. -
Filters always include the content type; nothing filters on
content_object=. -
Lists of generic objects use
prefetch_related()orGenericPrefetch, and query counts are asserted in tests. -
Content types are looked up with
get_for_model()orget_for_models(), not withget(). -
Fixtures use
--natural-foreign;remove_stale_contenttypesis run, reviewed, after models are deleted. -
Async code does not read
content_objectdirectly. -
Before adding a generic relation, a plain foreign key, a constrained set of nullable keys and inheritance were considered and rejected.