I have read the Django Docs regarding symmetrical=True
. I have also read this question asking the same question for an older version of Django but the following code is not working as the Django docs describe.
# 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)])
If bill
and ted
are friends, I would expect bill.friends.all()
to included ted
, and ted.friends.all()
to include bill
. This is not what happens. bill
's query includes ted
, but ted
's query does not include bill.
>>> 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 []>
Is this a bug or am I misunderstanding something?
EDIT: Updated code to show the behavior is the same with through_fields
set.
The proper way to add the relationship is bill.friends.add(ted)
. This will make bill
friends with ted
and ted
friends with bill
. If you want to set values for the extra fields on the intermediate model, in my case start
and end
, use the through_defaults
argument for add()
.
...
>>> bill.friends.add(ted, through_defaults={'start': datetime.now()}
There are cases where you want the relationship between bill
-> ted
to have different values on the intermediate model than ted
-> bill
. For example, bill
thinks ted
is "cool", when they first meet, but ted
thinks bill
is "mean". You'll need helper function in that case.
# people.models
from django.db import models
class Person(models.Model):
name = models.CharField(max_length=255)
friends = models.ManyToManyField("self", through='Friendship')
def __str__(self):
return self.name
def add_friendship(self, person, impressionA, impressionB, recursive=True):
self.friends.add(person, through_defaults={'personA_impression': impressionA, 'personB_impression': impressionB)
if recursive:
person.add_friendship(self, impressionB, impressionA, False)
class Friendship(models.Model):
personA = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='a')
personB = models.ForeignKey(Person, on_delete=models.CASCADE, related_name='b')
personA_impression = models.CharField(max_length=255)
personB_impression = models.CharField(max_length=255)
def __str__(self):
return ' and '.join([str(self.personA), str(self.personB)])
Calling bill.friends.add(ted, through_defaults={"personA_impression": "cool", "personB_impression": "mean"})
results in the following:
...
>>> bill_and_ted = Friendship.objects.get(personA=bill)
>>> ted_and_bill = Friendship.objects.get(personA=ted)
>>> bill_and_ted.personA_impression
"cool" # bill thinks ted is cool
>>> bill_and_ted.personB_impression
"mean" # ted thinks bill is mean
>>> ted_and_bill.personA_impression
"cool" # ted thinks bill is cool. This contradicts the bill_and_ted intermediate model
Using the add_friendship
function assigns the proper values to the fields.