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
I want to be able to get the relations between Food_Tags like:
>>> 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()
[]
but related_tags is empty for meat. I realize this has to do with the 'symmetrical=False' argument, but how can I set up the model such that 'meat.related_tags.all()' returns all related Food_Tags?
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.
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.