class Food_Tag(models.Model):
name = models.CharField(max_length=200)
related_tags = models.ManyToManyField(\'self\', blank=True, symmetrical=False, through=
Since you didn't explicitly say that they need to be asymmetrical, the first thing I'll suggest is setting As eternicode pointed out, you can't do this when you're using a symmetrical=True
. This will cause the relation to work both ways as you described.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()
.
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.
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.
I found this approach made by Charles Leifer which seems to be a good approach to overcome this Django limitation.
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.