M2M Relationships Explained¶
djangocms-custom-content provides flexible many-to-many relationships through two approaches that differ in direction and semantics:
invite_m2m_relations- Content model requests relations from target models (pull)relate_to- Model pushes relations onto target models (push)
The Through Model: custom_relation_factory¶
Traditional M2M relationships in Django use an automatic through model (join table) that links two specific models:
# Standard Django M2M
class Article(models.Model):
authors = models.ManyToManyField(Author) # Creates automatic through model
The through model has two ForeignKeys (one to each side).
Problem: This only works for relating to ONE specific model type. You can’t use one M2M field for relating to different model types.
Solution: custom_relation_factory
djangocms-custom-content creates a manual through model that uses:
One ForeignKey to the source model (specific)
One GenericForeignKey to ANY model (flexible)
This enables relating a source model to objects of ANY type.
How it Works
from djangocms_custom_content.models import custom_relation_factory
class PersonContent(AbstractCustomContent):
person = models.ForeignKey(Person, on_delete=models.CASCADE)
# ... other fields ...
# Manually create the through model for the Grouper (Person)
PersonRelation = custom_relation_factory(Person)
Generated Through Model Structure
The custom_relation_factory creates a relation model like:
class PersonContentRelation(AbstractCustomRelation):
# FK to source (specific model)
instance = models.ForeignKey(PersonContent, on_delete=models.CASCADE)
# Can relate to ANY model
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
The GenericForeignKey Magic
Field |
Purpose |
|---|---|
|
FK to PersonContent (always the same model) |
|
Stores which model type is related (BlogPost, Article, etc.) |
|
Stores the ID of the related object |
|
Virtual field that returns the actual object (BlogPost #5, Article #3, etc.) |
Example Data
One PersonContentRelation table can store relations to MULTIPLE model types:
-- Person 2 as author of BlogPost 1
INSERT INTO person_content_relation
(instance_id, content_type_id, object_id)
VALUES (2, 42, 1); -- content_type_id 42 = BlogPost
-- Person 2 as author of Article 5
INSERT INTO person_content_relation
(instance_id, content_type_id, object_id)
VALUES (2, 43, 5); -- content_type_id 43 = Article
-- Person 2 as author of Product 3
INSERT INTO person_content_relation
(instance_id, content_type_id, object_id)
VALUES (2, 44, 3); -- content_type_id 44 = Product
Single PersonContent can relate to thousands of BlogPosts, Articles, Products, etc.
Comparison: Django M2M vs. Generic M2M¶
Django M2M Example¶
# Separate through models needed for each relation type
class BlogPost(models.Model):
authors = models.ManyToManyField(Person) # Through: BlogPost_authors
class Article(models.Model):
authors = models.ManyToManyField(Person) # Through: Article_authors
class Product(models.Model):
creators = models.ManyToManyField(Person) # Through: Product_creators
Result: Three separate though tables, each only works for its specific pair.
Generic M2M Example¶
class PersonContent(AbstractCustomContent):
pass
# Single through model for ALL relations
# Note: Factory is created for the Grouper (Person), not the Content model
PersonRelation = custom_relation_factory(Person)
# PersonRelation table can manage relations to:
# - BlogPost
# - Article
# - Product
# - ANY Django model
Result: One table handles all relations!
How the Two Approaches Use custom_relation_factory¶
Both relate_to and invite_m2m_relations use the through model created by custom_relation_factory:
``invite_m2m_relations`` (Content Model Requests)
The source model knows about and requests relations from target models.
Semantics: “I invite you to provide relations for me. Do you have a suitable generic through table?”
Direction: Known target → Source
Flow:
Source model declares
invite_m2m_relationslisting targetscustom_relation_factorycreates through model for sourceCustomContentExtension reads the declaration
GenericM2MDescriptor is added to SOURCE model (not target)
If target unavailable: Creates dummy accessor (graceful degradation)
Example:
class BlogPostContent(AbstractCustomContent):
class CMSConfig:
# "I request relations from Person"
invite_m2m_relations = [("authors", "people.Person")]
# Creates the through model with GenericForeignKey
# The factory is for the target model (Person), not the source
PersonRelation = custom_relation_factory(Person)
Result:
blog_post = BlogPostContent.objects.first()
person = Person.objects.first()
# Source model gets the accessor (via GenericM2MDescriptor)
blog_post.authors.add(person)
blog_post.authors.all() # Uses PersonRelation table
Approach 2: relate_to (Model Pushes to Targets)¶
The source model pushes itself onto target models you have no control over.
Semantics: “I’m adding an accessor to you, so you can see me.”
Direction: Source → Multiple unknown targets
Flow:
Source model declares
relate_tolisting target pathscustom_relation_factorycreates through model for sourceregister_m2m_relations()is calledGenericM2MDescriptor is added to TARGET models (not source)
Target models remain unaware of source
Example:
class FlatCategory(AbstractCustomContent):
class CMSConfig:
# "I'm adding categories accessor to BlogPost and Article"
relate_to = [
("categories", "blog.BlogPost"),
("categories", "article.Article"),
]
# Creates the through model with GenericForeignKey
FlatCategoryRelation = custom_relation_factory(FlatCategory)
Result:
blog_post = BlogPost.objects.first()
article = Article.objects.first()
category = FlatCategory.objects.first()
# Target models get the accessor (via GenericM2MDescriptor)
blog_post.categories.add(category)
article.categories.add(category)
# Both use same FlatCategoryRelation table!
Design Differences¶
Aspect |
|
|
|---|---|---|
Who requests |
TARGET model |
SOURCE model |
Semantics |
“I request from you” |
“I push to you” |
Accessor added to |
TARGET model |
TARGET model(s) |
Source awareness |
Unaware of target |
Knows target |
Target awareness |
Expected to support it |
Unaware of source |
If target unavailable |
Dummy accessor created |
Raises improperly configured exception |
Typical sources |
Grouper models |
Grouper models |
Typical targets |
Content models |
Content models |
Accessing Relations¶
Both provide identical manager interfaces (backed by the through model):
# add() - Add objects
model.accessor.add(obj1, obj2)
# all() - Get all related
related = model.accessor.all()
# filter() - Filter related
related = model.accessor.filter(name="Django")
# remove() - Remove specific
model.accessor.remove(obj1)
# clear() - Remove all
model.accessor.clear()
# count() - Count relations
count = model.accessor.count()
# exists() - Check existence
if model.accessor.exists():
...
See Also¶
Set Up Many-to-Many Relations - Practical implementation guide
Model with M2M Relations - Step-by-step tutorial
Reference - API reference