How to make recursive ManyToMany relationships through an intermediate model symmetrical

535 views
Skip to first unread message

gjgi...@tutanota.com

unread,
Oct 16, 2020, 5:19:00 PM10/16/20
to Django Users
 

I've read the docs. I've read this question too, but the following code is not working as the Django docs describe.

If bill and ted are friends, bill.friends.all() should include ted,  and ted.friends.all() should include bill. This is not what Django does. ted's query is empty, while bill's query includes ted.
# people.models
from django.db import models


class Person(models.Model):
    name = models.CharField(max_length=255)
    friends = models.ManyToManyField("self",
                                     through='Friendship',
                                     through_fields=('personA', 'personB'),
                                     symmetrical=True,
                                     )

    def __str__(self):
        return self.name


class Friendship(models.Model):
    personA = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='personA')
    personB = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='personB')
    start = models.DateField(null=True, blank=True)
    end = models.DateField(null=True, blank=True)

    def __str__(self):
        return ' and '.join([str(self.personA), str(self.personB)])



>>> import django
>>> django.__version__
'3.1.2' >>>
from people.models import Person, Friendship >>> bill = Person(name='bill') >>> bill.save() >>> ted = Person(name='ted') >>> ted.save() >>> bill_and_ted = Friendship(personA=bill, personB=ted) >>> bill_and_ted.save() >>> bill.friends.all() <QuerySet [<Person: ted>]> >>> ted.friends.all() <QuerySet []> >>> ted.refresh_from_db() >>> ted.friends.all() <QuerySet []> >>> ted = Person.objects.get(name='ted') >>> ted.friends.all() <QuerySet []>

Can someone please show me how to make this behave as expected.

David Nugent

unread,
Oct 16, 2020, 7:05:51 PM10/16/20
to django...@googlegroups.com
This is expected with your code.  You've created an asymmetric relationship from bill to ted, but not the reverse. This would be appropriate in a "follow" relationship. For symmetric relationships you need to create records in both directions. There are a few ways to do this but a helper function on the Person model is the most direct approach, something along the lines:

    def add_friendship(self, person, symmetric=True):
        friendship = Friendship.objects.get_or_create(personA=self, personB=person)
        if symmetric:
            # avoid recursion
            person.add_friendship(self, False)
        return friendship


Regards, David

--
You received this message because you are subscribed to the Google Groups "Django users" group.
To unsubscribe from this group and stop receiving emails from it, send an email to django-users...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/django-users/MJmh5Qw--3-2%40tutanota.com.

coolguy

unread,
Oct 16, 2020, 7:28:32 PM10/16/20
to Django users

With your example, you can also find the records through>>> ted.person_set.all().

David Nugent

unread,
Oct 16, 2020, 7:50:01 PM10/16/20
to django...@googlegroups.com
Just to add, I don't think django supports symmetrical M2M relations with additional data / explicit through model without the shim I suggested.

For example, this works:

from django.db import models


class Person(models.Model):
name = models.CharField(max_length=255)
    friends = models.ManyToManyField("self", symmetrical=True)

def __str__(self):
return self.name

>>> from people.models import Person
>>> bill = Person.objects.create(name='bill')
>>> ted = Person.objects.create(name='ted')
>>> bill.friends.add(ted)
>>> bill.friends.all()
<QuerySet [<Person: ted>]>
>>> ted.friends.all()
<QuerySet [<Person: bill>]>





gjgi...@tutanota.com

unread,
Oct 16, 2020, 8:21:50 PM10/16/20
to Django Users
Thanks for all for the replies!

@David, the helper function works as expected.

>>> from people.models import Person, Friendship
>>> bill = Person(name='bill')
>>> bill.save()
>>> ted = Person(name='ted')
>>> ted.save()
>>> bill.add_friendship(ted, True)
(<Friendship: bill and ted>, True)
>>> bill.friends.all()
<QuersySet [<Person: ted>]>
>>> ted.friends.all()
<QuerySet [<Person: bill>]>


Also, @coolguy for my code, the correct call is >>> ted.personB.all() without the helper function. ted.personA.all() returns an empty queryset without the helper function.

"Recursive relationships using an intermediary model and defined as symmetrical (that is, with symmetrical=True, which is default) can't determine the accessory names, as they would be the same. You need to set a related_name to at least one of them. If you'd prefer Django not to create a backwards relation, set related_name to '+'."

This implies Django makes the reverse relation by default. Am I misunderstanding something?


Oct 16, 2020, 16:48 by dav...@uniquode.io:

David Nugent

unread,
Oct 16, 2020, 11:11:26 PM10/16/20
to django...@googlegroups.com
After playing with this to answer your question and to correct my previous response, I found that it does work as documented when using a "through" model without using "through_fields".

from django.db import models


class Person(models.Model):
name = models.CharField(max_length=255)
    friends = models.ManyToManyField("self", through='Friendship', symmetrical=True)

def __str__(self):
return self.name


class Friendship(models.Model):
person_a = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='a')
person_b = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='b')
start = models.DateField(auto_now_add=True)

def __str__(self):
return f'{self.person_a.name} => {self.person_b.name}: {self.start}'

>>> from people.models import Person, Friendship
>>> bill = Person.objects.create(name='bill')
>>> rufus = Person.objects.create(name='rufus')
>>> bill.friends.add(ted)
>>> bill.friends.add(rufus)
>>> rufus.friends.add(bill)
>>> rufus.friends.add(ted)
>>> bill.friends.all()
<QuerySet [<Person: ted>, <Person: rufus>]>
>>> ted.friends.all()
<QuerySet [<Person: bill>, <Person: rufus>]>
>>> rufus.friends.all()
<QuerySet [<Person: bill>, <Person: ted>]>
>>> Friendship.objects.all()
<QuerySet [
  <Friendship: bill => ted: 2020-10-17>, 
  <Friendship: ted => bill: 2020-10-17>,
  <Friendship: bill => rufus: 2020-10-17>,
  <Friendship: rufus => bill: 2020-10-17>,
  <Friendship: rufus => ted: 2020-10-17>,
  <Friendship: ted => rufus: 2020-10-17>
]>


In your case, naming the related_name the same as the field name may be an issue. Since related_name becomes a pseudo field on the model in which it is defined, so there is a potential clash in the namespace?
Reply all
Reply to author
Forward
0 new messages