Fix Django self-referential ManyToMany through_fields
Swap through_fields order to fix reversed user_from/user_to in Django self-referential ManyToMany through model. Examples, migration steps, and data-fix tips.
Django ManyToMany self-referential relationship with through model: user_from and user_to fields assigned in reverse
I implemented a followers/following relationship using an intermediate through model in Django User model. However, the fields in the Contact model are populated oppositely to my expectation: the follower should be in user_from and the followee in user_to, but it’s reversed.
User model:
class User(AbstractUser):
# some fields
following = models.ManyToManyField(
"self",
related_name='followers',
through="Contact",
through_fields=('user_to', 'user_from'),
symmetrical=False,
blank=True
)
Contact through model:
class Contact(models.Model):
user_from = models.ForeignKey(User, related_name="rel_from_set", on_delete=models.CASCADE)
user_to = models.ForeignKey(User, related_name="rel_to_set", on_delete=models.CASCADE)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created']
indexes = [models.Index(fields=['created'])]
def __str__(self):
return f"{self.user_from} follows {self.user_to}"
For example, when user A follows user B, user A ends up in user_to and user B in user_from. How do I configure the through_fields or models to correctly assign follower to user_from and followee to user_to?
Your reversal happens because Django maps the model that declares the ManyToManyField (the “source”) to the first name you give in through_fields. You used through_fields=(‘user_to’, ‘user_from’), so Django treated user_to as the source — hence the flipped assignment. Swap the tuple to through_fields=(‘user_from’, ‘user_to’), run makemigrations/migrate, and your user.following.add(other) calls will create Contact(user_from=caller, user_to=target) as expected. Also add a unique constraint and test or migrate existing rows if you already have data.
Contents
- Correcting through_fields order in Django many to many (self-referential)
- Example: models setup for followers/following through model
- Why the fields were reversed (how through_fields mapping works)
- Testing and queries (add, create, check Contact entries)
- Fixing existing reversed data (database migration example)
- Common pitfalls and best practices for through models
- Sources
- Conclusion
Correcting through_fields order in Django many to many (self-referential)
What to change: in your ManyToManyField declaration the first item in through_fields must be the field on the through model that points to the model declaring the ManyToManyField (the source), and the second item must point to the target. You currently have them reversed.
Fix: swap the tuple to (‘user_from’, ‘user_to’).
Corrected snippet:
class User(AbstractUser):
# some fields
following = models.ManyToManyField(
"self",
related_name='followers',
through="Contact",
through_fields=('user_from', 'user_to'), # <- corrected order
symmetrical=False,
blank=True,
)
After changing the code: run
If migrations complain, double-check that Contact exists and that the names in through_fields exactly match the ForeignKey field names on Contact.
(For reference on through_fields semantics see the Django docs linked below.)
Example: models setup for followers/following through model
A full, recommended setup including a uniqueness constraint and tidy related_names:
from django.contrib.auth import get_user_model
from django.db import models
User = get_user_model()
class Contact(models.Model):
user_from = models.ForeignKey(
User, related_name="rel_from_set", on_delete=models.CASCADE
)
user_to = models.ForeignKey(
User, related_name="rel_to_set", on_delete=models.CASCADE
)
created = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created']
indexes = [models.Index(fields=['created'])]
constraints = [
models.UniqueConstraint(fields=['user_from', 'user_to'], name='unique_contact')
]
def __str__(self):
return f"{self.user_from} follows {self.user_to}"
class User(AbstractUser):
following = models.ManyToManyField(
"self",
related_name='followers',
through=Contact,
through_fields=('user_from', 'user_to'),
symmetrical=False,
blank=True,
)
Why this is nice: user.following.all() returns the users you follow; user.followers.all() returns users that follow you. The UniqueConstraint prevents duplicate follow rows.
The blog post by Charles Leifer has a clear follower/following example if you want another worked example: https://charlesleifer.com/blog/self-referencing-many-many-through/
Why the fields were reversed (how through_fields mapping works)
Short answer: Django reads through_fields in order: first entry = FK on the through model that points to the source (the model where the ManyToManyField is declared), second entry = FK that points to the target.
So with your original code:
through_fields=(‘user_to’, ‘user_from’)
Django treated user_to as the source. Thus:
- userA.following.add(userB) → Contact.user_to = userA, Contact.user_from = userB
Swap to (‘user_from’,‘user_to’) and the same call will produce:
- userA.following.add(userB) → Contact.user_from = userA, Contact.user_to = userB
Django’s own docs explain the tuple ordering and why it’s required for self-referential through models (both FKs point to the same model): https://docs.djangoproject.com/en/5.1/topics/db/models/#many-to-many-relationships
Stack Overflow threads and the Django ticket history show this is a common source of confusion — many answers point out that the order in through_fields, not the textual order of field definitions, controls the mapping (example: https://stackoverflow.com/questions/3880489/how-do-i-write-a-django-model-with-manytomany-relationsship-with-self-through-a).
Testing and queries (add, create, check Contact entries)
Quick checks after you change through_fields:
- Create two users in shell:
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model()
>>> a = User.objects.create(username='alice')
>>> b = User.objects.create(username='bob')
- Use the manager:
>>> a.following.add(b) # creates Contact(user_from=a, user_to=b)
>>> Contact.objects.filter(user_from=a, user_to=b).exists() # True
>>> list(a.following.all()) # [<User: bob>]
>>> list(b.followers.all()) # [<User: alice>]
- Creating manually (useful when the through model has extra required fields):
Contact.objects.create(user_from=a, user_to=b)
Notes:
- If your through model has extra non-nullable fields without defaults, user.following.add(…) won’t work because Django can’t supply those extra values; in that case create the Contact explicitly.
- You can reference the through model at runtime with User.following.through.
Fixing existing reversed data (database migration example)
If you already have Contact rows that are flipped, you’ll need to fix the rows in the DB. Backup first. Two safe approaches:
Option A — create new correct rows then delete old ones (handles unique constraints carefully):
from django.db import transaction
with transaction.atomic():
for c in Contact.objects.all():
# only create the corrected row if it doesn't already exist
if not Contact.objects.filter(user_from=c.user_to, user_to=c.user_from).exists():
Contact.objects.create(
user_from=c.user_to,
user_to=c.user_from,
created=c.created,
)
# after creating corrected rows, delete the old reversed ones
Contact.objects.filter(user_from__username__in=[...]) # carefully target what you want
Option B — swap columns in SQL using a temp placeholder (fast, but be careful with uniqueness and FK constraints). Example (PostgreSQL sketch):
UPDATE contacts SET user_from = NULL WHERE …; – break FK temporarily (or disable constraints)
UPDATE contacts SET user_from = user_to WHERE user_from IS NULL;
UPDATE contacts SET user_to = old_user_from_value; – requires temp column
– then re-enable constraints
Because SQL steps vary by DB and constraints, prefer the ORM approach unless you know your DB well. Always test on a copy and ensure you don’t create duplicates that violate uniqueness.
Common pitfalls and best practices for through models
- Wrong through_fields order — the usual culprit. First = source, second = target.
- Field names must match exactly the ForeignKey attribute names on the through model.
- Don’t assume class attribute order in Contact controls mapping — it doesn’t; through_fields does.
- Remember symmetrical=False for follower/following relationships (default for self-M2M is symmetrical=True).
- If the through model has required extra fields, user.following.add() will fail — create the through-row manually.
- Add a UniqueConstraint (or unique_together for older Django) on (user_from, user_to) to prevent duplicates.
- After changing code, run makemigrations/migrate and test in the shell.
- Choose clear field names (user_from/user_to or follower/followed) and matching related_name values to avoid confusion.
Sources
- Django docs — Many-to-many relationships: https://docs.djangoproject.com/en/5.1/topics/db/models/#many-to-many-relationships
- Charles Leifer — Self-referencing many-to-many through: https://charlesleifer.com/blog/self-referencing-many-many-through/
- Stack Overflow — How do I write a Django model with ManyToMany relationsship with self through a Model: https://stackoverflow.com/questions/3880489/how-do-i-write-a-django-model-with-manytomany-relationsship-with-self-through-a
- Stack Overflow — Django many-to-many self-reference ORM discussion: https://stackoverflow.com/questions/45847930/django-in-many-to-many-relationship-within-the-self-class-how-do-i-reference-e
- Stack Overflow — Many to many self-reference with through model example: https://stackoverflow.com/questions/55292675/django-orm-many-to-many-self-reference-with-through-model
- Django ticket #26352 — through_fields discussion: https://code.djangoproject.com/ticket/26352
Conclusion
Swap your through_fields to (‘user_from’, ‘user_to’) so the source (the model declaring the field) maps to user_from and the target maps to user_to; then run migrations and test with user.following.add(other). This resolves the django many to many self-referential mapping issue and gives you predictable follower → followee rows in your through model.