How to make recursive ManyToManyField relationships that have extra fields symmetrical in Django?

一笑奈何 提交于 2019-11-29 07:55:49

As mentioned in the docs:

Thus, it is not (yet?) possible to have a symmetrical, recursive many-to-many relationship with extra fields, in Django. It's a "pick two" sorta deal.

ePascoal

I found this approach made by Charles Leifer which seems to be a good approach to overcome this Django limitation.

Since you didn't explicitly say that they need to be asymmetrical, the first thing I'll suggest is setting symmetrical=True. This will cause the relation to work both ways as you described. As eternicode pointed out, you can't do this when you're using a through model for the M2M relationship. If you can afford to go without the through model, you can set symmetrical=True to get exactly the behavior you describe.

If they need to remain asymmetrical however, you can add the keyword argument related_name="sources" to the related_tags field (which you might want to consider renaming to targets to make things more clear) and then access the related tags using meat.sources.all().

To create a symmetrical relationship, you have two options:

1) Create two Tag_Relation objects - one with steak as the source, and another with steak as the target:

>>> steak = Food_Tag.objects.create(name="steak")
>>> meat = Food_Tag.objects.create(name="meat")
>>> r1 = Tag_Relation(source=steak, target=meat, is_a=True)
>>> r1.save()
>>> r2 = Tag_Relation(source=meat, target=steak, has_a=True)
>>> r2.save()
>>> steak.related_tags.all()
[<Food_Tag: meat>]
>>> meat.related_tags.all()
[<Food_Tag: steak]

2) Add another ManyToManyField to the Food_Tag model:

class Food_Tag(models.Model):
    name = models.CharField(max_length=200)
    related_source_tags = models.ManyToManyField('self', blank=True, symmetrical=False, through='Tag_Relation', through_fields=('source', 'target'))
    related_target_tags = models.ManyToManyField('self', blank=True, symmetrical=False, through='Tag_Relation', through_fields=('target', 'source'))

class Tag_Relation(models.Model):
    source = models.ForeignKey(Food_Tag, related_name='source_set')
    target = models.ForeignKey(Food_Tag, related_name='target_set')

As a note, I'd try to use something more descriptive than source and target for your through model fields.

The best solution of this problem (after many investigations) was to manually create symmetrical db record on save() call. This results in DB data redundancy, of course, because you create 2 records instead of one. In your example, after saving Tag_Relation(source=source, target=target, ...) you should save reverse relation Tag_Relation(source=target, target=source, ...) like this:

class Tag_Relation(models.Model):
    source = models.ForeignKey(Food_Tag, related_name='source_set')
    target = models.ForeignKey(Food_Tag, related_name='target_set')
    is_a = models.BooleanField(default=False);
    has_a = models.BooleanField(default=False);

    class Meta:
        unique_together = ('source', 'target')

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)

        # create/update reverse relation using pure DB-level functions
        # we cannot just save() reverse relation because there will be a recursion
        reverse = Tag_Relation.objects.filter(source=self.target, target=self.source)
        if reverse.exists():
            reverse.update(is_a=self.is_a, has_a=self.has_a)
        else:
            Tag_Relation.objects.bulk_create([
                Tag_Relation(source=self.target, target=self.source, is_a=self.is_a, has_a=self.has_a)
            ])

The only disadvantage of this implementation is duplicating Tag_Relation entry, but except this everything works fine, you can even use Tag_Relation in InlineAdmin.

UPDATE Do not forget to define delete method as well which will remove reverse relation.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!