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

前端 未结 5 724
后悔当初
后悔当初 2020-12-18 07:53
class Food_Tag(models.Model):
    name = models.CharField(max_length=200)
    related_tags = models.ManyToManyField(\'self\', blank=True, symmetrical=False, through=         


        
相关标签:
5条回答
  • 2020-12-18 08:29

    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().

    0 讨论(0)
  • 2020-12-18 08:30

    As mentioned in the docs:

    • When defining a many-to-many relationship from a model to itself, using an intermediary model, you must use symmetrical=False (see the model field reference).

    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.

    0 讨论(0)
  • 2020-12-18 08:30

    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.

    0 讨论(0)
  • 2020-12-18 08:32

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

    0 讨论(0)
  • 2020-12-18 08:41

    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.

    0 讨论(0)
提交回复
热议问题