Sometimes one model needs to relate to many different models at once. A Tag should be attachable to articles, videos and products; a Like, Comment or Attachment should work on any model in your app. Django's built-in ManyToManyField can't express this — it points at a single target model. The fix is a generic many-to-many relationship built on the contenttypes framework.
This post focuses on the many-to-many generic pattern: relating one model to an open set of other models. If the underlying building block is new to you, read Understanding GenericForeignKey in Django first — here we assume the basics and build a reusable, cross-model tagging system on top of them.
Why ManyToManyField falls short
A standard ManyToManyField(Article) creates a join table with two concrete foreign keys: one to the source row and one to Article. There is no way to make that second column point at Article today, Video tomorrow. To relate one model to a varying set of targets you need a join row whose target is described by two columns instead of one:
content_type— a foreign key todjango.contrib.contenttypes.models.ContentType, identifying which model the row points at.object_id— the primary key value of the target row.content_object— aGenericForeignKeythat ties those two together into a normal object accessor.
That trio lives on an intermediary (through) model, and each taggable model exposes a GenericRelation for convenient reverse access.
1. Enable contenttypes
The framework ships with Django and is enabled in new projects by default. Confirm it is in INSTALLED_APPS:
# settings.py
INSTALLED_APPS = [
# ...
"django.contrib.contenttypes",
"django.contrib.auth", # depends on contenttypes
# ...
"tagging", # the app that will hold Tag + TaggedItem
]2. The through model with a GenericForeignKey
TaggedItem is the join model. It carries an ordinary FK to Tag plus the generic trio that can point at any model. Two details matter for production: the composite index on (content_type, object_id) — the lookup you will run constantly — and a UniqueConstraint so the same tag can't be attached to the same object twice.
# tagging/models.py
from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
class Tag(models.Model):
name = models.SlugField(unique=True)
def __str__(self):
return self.name
class TaggedItem(models.Model):
"""Links one Tag to an instance of ANY model."""
tag = models.ForeignKey(Tag, related_name="tagged_items", on_delete=models.CASCADE)
# The three pieces that make up a GenericForeignKey:
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveBigIntegerField() # match your target PK type
content_object = GenericForeignKey("content_type", "object_id")
class Meta:
indexes = [models.Index(fields=["content_type", "object_id"])]
constraints = [
models.UniqueConstraint(
fields=["tag", "content_type", "object_id"],
name="unique_tag_per_object",
),
]
def __str__(self):
return f"{self.tag} -> {self.content_object}"3. Add a GenericRelation to taggable models
GenericRelation is the reverse accessor. It lets each model read and write its own tags as if it were an ordinary relation and — importantly — makes Django delete the related TaggedItem rows when the object is deleted. related_query_name is the name you use to filter back from the TaggedItem side.
# blog/models.py
from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from tagging.models import TaggedItem
class Article(models.Model):
title = models.CharField(max_length=200)
tags = GenericRelation(TaggedItem, related_query_name="article")
def __str__(self):
return self.title
class Video(models.Model):
title = models.CharField(max_length=200)
tags = GenericRelation(TaggedItem, related_query_name="video")
def __str__(self):
return self.titleCreate and run the migrations. The contenttypes table is populated automatically as Django discovers your installed models:
python manage.py makemigrations
python manage.py migrate4. Attaching tags
A tiny helper keeps the ContentType lookup in one place. ContentType.objects.get_for_model() is cached, so this stays cheap even in loops:
from django.contrib.contenttypes.models import ContentType
from blog.models import Article, Video
from tagging.models import Tag, TaggedItem
def add_tag(obj, name):
tag, _ = Tag.objects.get_or_create(name=name)
TaggedItem.objects.get_or_create(
tag=tag,
content_type=ContentType.objects.get_for_model(obj),
object_id=obj.pk,
)
article = Article.objects.create(title="Generic relations in Django")
video = Video.objects.create(title="Django ORM deep dive")
add_tag(article, "django")
add_tag(article, "orm")
add_tag(video, "django")5. Querying both directions
Forward — the tags on a single object use the GenericRelation accessor, which behaves like a normal related manager:
# TaggedItem rows attached to this article
article.tags.all()
[ti.tag.name for ti in article.tags.all()]
# ['django', 'orm']Reverse — every object that carries a given tag. Because the targets live in different tables, filter TaggedItem by content_type and resolve the matching rows per model:
from django.contrib.contenttypes.models import ContentType
tag = Tag.objects.get(name="django")
# All join rows for this tag, across every model
tag.tagged_items.all()
# Just the Articles tagged "django"
ct = ContentType.objects.get_for_model(Article)
ids = TaggedItem.objects.filter(tag=tag, content_type=ct).values_list("object_id", flat=True)
Article.objects.filter(pk__in=ids)
# Or, using related_query_name, straight from the Article side:
Article.objects.filter(tags__tag__name="django").distinct()Filtering by the raw content_type + object_id pair is the workhorse lookup — it is exactly what the composite index from step 2 serves:
ct = ContentType.objects.get_for_model(article)
TaggedItem.objects.filter(content_type=ct, object_id=article.pk)6. Avoid N+1 queries
A GenericForeignKey is not a real SQL join, so select_related("content_object") does nothing — iterating content_object naively fires one query per row. Use prefetch_related, and for the GFK target specifically Django 4.2+ ships GenericPrefetch, which lets you scope exactly which models to fetch:
from django.contrib.contenttypes.prefetch import GenericPrefetch
# Batches the target lookups instead of one query per row (Django 4.2+)
items = TaggedItem.objects.select_related("tag").prefetch_related(
GenericPrefetch("content_object", [Article.objects.all(), Video.objects.all()])
)
for item in items:
print(item.tag.name, "->", item.content_object)
# From the object side, prefetch the join rows and their tags in one go:
Article.objects.prefetch_related("tags__tag")Caveats to know
- No database-level integrity.
object_idis just an integer column — there is no real foreign key, so the database can't cascade deletes or block orphans. Cleanup is handled in Python via theGenericRelation; if you bulk-delete targets that don't declare one, or delete with raw SQL, you'll leave orphanedTaggedItemrows. - Limited joins. A
GenericForeignKeycan't be used withselect_related, and you can'tJOINacross it the way you can a normal FK. Reach forprefetch_related/GenericPrefetchinstead. - PK type must match.
object_idhas to hold the target's primary key. Django's defaultBigAutoFieldPKs needPositiveBigIntegerField; UUID PKs need aUUIDField(orCharField) and aGenericForeignKeypointed at it. A mismatched type silently fails to match rows. - More indexes, more care. That
(content_type, object_id)index is mandatory for performance — see Django database access optimization for the wider query-tuning picture.
Built-in pattern vs a library
The contenttypes approach above is dependency-free and lets you add fields to the through model (who tagged something, when, a relevance score). Two libraries can save the boilerplate:
django-taggit— the go-to when you specifically need tagging. Jazzband-maintained, ships aTaggableManager, and supports custom through models when you outgrow the defaults.django-gm2m/django-generic-m2m— expose a singleGM2MFieldthat hides the through model and adds reverse relations, prefetching and configurable deletion. Convenient, but check the project's recent activity and confirm Django 5.x support before depending on it.
Use the built-in pattern when you want zero dependencies, full control, or extra columns on the join row. Use a library when you need standard tagging quickly and don't want to maintain the plumbing. Either way, pairing the relation with custom model managers and model managers and properties keeps the query logic readable.
Generic many-to-many relations are one of Django's most powerful — and most over-used — features. Applied deliberately, with the right indexes and prefetching, they give you flexible, reusable, cross-model relations without bending your schema out of shape. If you'd rather have a team own the heavy ORM modelling, our Django development services can help.
Frequently Asked Questions
What is a generic many-to-many relationship in Django?
It is a pattern that lets one model relate to instances of many different models through an intermediary (through) model. Instead of a fixed foreign key, the through model uses a GenericForeignKey (content_type + object_id), so a single Tag, Like or Attachment can attach to articles, videos, products and anything else.
Why can't I just use ManyToManyField?
ManyToManyField targets exactly one model — its join table has a concrete foreign key to that model. It cannot point at a varying set of targets. The generic approach replaces that single FK with a content_type/object_id pair so the target can be any model in your project.
Do I need a custom through model, or can a library handle it?
Both work. The built-in contenttypes pattern is dependency-free and lets you add fields to the join row (timestamps, scores, who tagged it). Libraries such as django-taggit (for tagging) or django-gm2m (a generic GM2MField) remove boilerplate — choose them when your needs are standard and you don't want to maintain the plumbing yourself.
How do I avoid N+1 queries with GenericForeignKey?
select_related does not work on a GFK because it isn't a real join. Use prefetch_related, and on Django 4.2+ use GenericPrefetch to scope exactly which models to load for content_object. From the object side, prefetch the join rows and their tags, e.g. prefetch_related("tags__tag").
Does a generic relation enforce referential integrity?
No. object_id is a plain integer with no database foreign key, so the database can't cascade deletes or prevent orphans. A GenericRelation on the target model makes Django clean up join rows on delete, but bulk or raw deletes can still leave orphans — handle those in application code.
What primary-key type should object_id use?
It must match the target's primary key. With Django's default BigAutoField, use PositiveBigIntegerField. For UUID primary keys, use a UUIDField (or CharField) and point the GenericForeignKey at it. Mismatched types silently fail to match any rows.