Как сделать рекурсивные отношения ManyToManyField с дополнительными полями симметричными в Django? - PullRequest
4 голосов
/ 09 ноября 2010
class Food_Tag(models.Model):
    name = models.CharField(max_length=200)
    related_tags = models.ManyToManyField('self', blank=True, symmetrical=False, through='Tag_Relation')

    def __unicode__(self):
     return self.name

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); # True if source is a target
    has_a = models.BooleanField(default=False); # True if source has a target

Я хочу иметь возможность получить отношения между Food_Tags вроде:

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

но related_tags пуст для мяса. Я понимаю, что это связано с аргументом «симметричный = ложный», но как я могу настроить модель таким образом, чтобы функция «meat.related_tags.all ()» возвращала все связанные теги Food_Tags?

Ответы [ 5 ]

5 голосов
/ 09 ноября 2010

Как уже упоминалось в документах :

Таким образом, в Джанго невозможно (пока?) Иметь симметричную, рекурсивную взаимосвязь «многие ко многим» с дополнительными полями. Это сделка «выбери два».

2 голосов
/ 13 апреля 2015

Я нашел этот подход, сделанный Чарльзом Лейфером , который кажется хорошим подходом для преодоления этого ограничения Джанго.

1 голос
/ 09 ноября 2010

Поскольку вы явно не сказали, что они должны быть асимметричными, первое, что я предлагаю, это установить symmetrical=True.Это приведет к тому, что отношение будет работать в обоих направлениях, как вы описали. Как указывал eternicode, вы не можете сделать это, когда используете модель through для отношения M2M.Если вы можете позволить себе обходиться без модели through, вы можете установить symmetrical=True, чтобы получить именно то поведение, которое вы описываете.

Если они должны оставаться асимметричными, вы можете добавить аргумент ключевого слова related_name="sources"в поле related_tags (которое можно рассмотреть, переименовав в targets, чтобы сделать вещи более понятными), а затем получить доступ к связанным тегам, используя meat.sources.all().

0 голосов
/ 14 октября 2016

Лучшее решение этой проблемы (после многих исследований) состояло в том, чтобы вручную создать симметричную запись БД при вызове save().Это приводит к избыточности данных в БД, потому что вы создаете 2 записи вместо одной.В вашем примере после сохранения Tag_Relation(source=source, target=target, ...) вы должны сохранить обратную связь Tag_Relation(source=target, 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')
    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)
            ])

Единственным недостатком этой реализации является дублирование записи Tag_Relation, но кроме этого все работает нормально, выможно даже использовать Tag_Relation в InlineAdmin.

UPDATE Не забудьте также определить метод delete, который удалит обратную связь.

0 голосов
/ 04 мая 2015

Чтобы создать симметричные отношения, у вас есть два варианта:

1) Создайте два объекта Tag_Relation - один с steak в качестве источника, а другой с steak в качестве цели:

>>> 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) Добавьте еще одно ManyToManyField к модели Food_Tag:

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')

Как примечание, я бы попытался использовать что-то более описательное, чем source и target для ваших сквозных полей модели.

...