Web

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.

1 answer 1 view

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:

python
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:

python
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)

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:

python
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:

python
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:

  1. Create two users in shell:
python
>>> from django.contrib.auth import get_user_model
>>> User = get_user_model()
>>> a = User.objects.create(username='alice')
>>> b = User.objects.create(username='bob')
  1. Use the manager:
python
>>> 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>]
  1. Creating manually (useful when the through model has extra required fields):
python
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):

python
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


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.

Authors
Verified by moderation
Moderation
Fix Django self-referential ManyToMany through_fields