Reputation: 9163
Let's use the classic example of friends.
class Friendship(models.Model):
user1 = models.ForeignKey(User, related_name='friends1')
user2 = models.ForeignKey(User, related_name='friends2')
handshakes = models.PositiveIntegerField()
hugs = models.PositiveIntegerField()
# other silly data
Two friends in a friendship (user1 and user2) should be completely equal. I should be able to say that (user1, user2) are unique_together and not have to worry about (user2, user1) accidentally showing up. I should be able to get all the friends of a given user easily, but instead I'd have to write a custom manager or create some other way of getting all the Friendships where that user is user1 in the relationship, and all the Friendships where that user is user2.
I'm considering trying to write my own SymmetricKey. Someone please stop me.
Upvotes: 22
Views: 4332
Reputation: 319
I found a nice article discussing that some time ago, the basics are the following:
class Person(models.Model):
name = models.CharField(max_length=100)
relationships = models.ManyToManyField('self', through='Relationship',
symmetrical=False,
related_name='related_to+')
RELATIONSHIP_FOLLOWING = 1
RELATIONSHIP_BLOCKED = 2
RELATIONSHIP_STATUSES = (
(RELATIONSHIP_FOLLOWING, 'Following'),
(RELATIONSHIP_BLOCKED, 'Blocked'),
)
class Relationship(models.Model):
from_person = models.ForeignKey(Person, related_name='from_people')
to_person = models.ForeignKey(Person, related_name='to_people')
status = models.IntegerField(choices=RELATIONSHIP_STATUSES)
Note the plus-sign at the end of related_name. This indicates to Django that the reverse relationship should not be exposed. Since the relationships are symmetrical, this is the desired behavior, after all, if I am friends with person A, then person A is friends with me. Django won't create the symmetrical relationships for you, so a bit needs to get added to the add_relationship and remove_relationship methods to explicitly handle the other side of the relationship:
def add_relationship(self, person, status, symm=True):
relationship, created = Relationship.objects.get_or_create(
from_person=self,
to_person=person,
status=status)
if symm:
# avoid recursion by passing `symm=False`
person.add_relationship(self, status, False)
return relationship
def remove_relationship(self, person, status, symm=True):
Relationship.objects.filter(
from_person=self,
to_person=person,
status=status).delete()
if symm:
# avoid recursion by passing `symm=False`
person.remove_relationship(self, status, False)
Now, whenever we create a relationship going one way, its complement is created (or removed). Since the relationships go in both directions, we can simply use:
def get_relationships(self, status):
return self.relationships.filter(
to_people__status=status,
to_people__from_person=self)
Source: Self-referencing many-to-many through
Upvotes: 4
Reputation: 787
Check out the symmetrical
option of ManyToManyField
in the docs -- sounds like it can do what you want.
For the specific way you're doing it, I'd do something like
class LameUserExtension(User):
friends = ManyToManyField("self", through=Friendship)
class Friendship(models.Model):
# the stuff you had here
Upvotes: 8