简体   繁体   中英

How to make a recursive ManyToMany relationship symmetrical with Django

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM