Creating recursive, symmetrical many-to-many relationships in Django
In Django, a recursive many-to-many relationship is a ManyToManyField
that points to the same model in which it's defined ('self')
. A
symmetrical relationship is one in where, when a.contacts = [b]
, a is
in b.contacts
.
In changeset 8136, support for through models was added to the Django core. This allows you to create a many-to-many relationship that goes through a model of your choice:
class Contact(models.Model):
contacts = models.ManyToManyField('self', through='ContactRelationship',
symmetrical=False)
class ContactRelationship(models.Model):
types = models.ManyToManyField('RelationshipType', blank=True,
related_name='contact_relationships')
from_contact = models.ForeignKey('Contact', related_name='from_contacts')
to_contact = models.ForeignKey('Contact', related_name='to_contacts')
class Meta:
unique_together = ('from_contact', 'to_contact')
According to the Django
Docs,
you must set symmetrical=False
for recursive many-to-many
relationships that use an intermediary model. Sometimes--for a recent
case in django-crm, for
example--what you really want is a symmetrical, recursive
many-to-many relationship.
The trick to getting this working is understanding what
symmetrical=True
actually does. From what we can tell after a brief
look through the Django core, symmetrical=True
is simply a utility
that (a) creates a second, reverse relationship in the many-to-many
table, and (b) hides the field in the related model (in this case the
same model) from use by appending a '+' to its name.
Since you normally have to create many-to-many relationships manually
when a through
model is specified, the solution is simply to leave
symmetrical=False
(otherwise it'll raise an exception) and create the
reverse relationship manually yourself via the through
model:
crm.ContactRelationship.objects.create(from_contact=contact_a, to_contact=contact_b)
crm.ContactRelationship.objects.create(from_contact=contact_b, to_contact=contact_a)
Additionally, you'll have to do a little cleanup to make sure both
sides of the relationship are removed when one is removed, but otherwise
this should achieve the same effect as setting symmetrical=True
in
other many-to-many relationships.
To hide the other side of the related manager, you can append a '+' to the related_name, like so:
class Contact(models.Model):
contacts = models.ManyToManyField('self', through='ContactRelationship',
symmetrical=False,
related_name='related_contacts+')
Good luck and feel free to comment with any questions!