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.
From the documentation :
When you have more than one foreign key on an intermediary model to any (or even both) of the models participating in a many-to-many relationship, you must specify
through_fields
. This also applies to recursive relationships when an intermediary model is used and there are more than two foreign keys to the model, or you want to explicitly specify which two Django should use.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.