tortoise / tortoise-orm

Familiar asyncio ORM for python, built with relations in mind
https://tortoise.github.io
Apache License 2.0
4.38k stars 356 forks source link

Support symmetrical ManyToMany relationships #1612

Open laggron42 opened 1 month ago

laggron42 commented 1 month ago

Django ORM has support for what they call symmetrical relationships, that is a ManyToMany relationship on self. (see here https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.ManyToManyField.symmetrical)

For example, if you want a ManyToMany relation for friends and you want the logic to be "if I am your friend, then you are my friend", you would use a symmetrical relation. That way, there is no related attribute generated on the other class, and for checking the friend status, you only have to look up one field instead of both.

Example

class User(models.Model):
  name = fields.TextField()
  friends = fields.ManyToMany("models.User", symmetrical=True)

>>> u1 = await User.create("Bob")
>>> u2 = await User.create("Alice")
>>> await u1.friends.add(u2)
>>> await u1.friends.filter(id=u2.id).exists()
True
>>> # bi-directional
>>> await u2.friends.filter(id=u1.id).exists()
True

I can try to contribute this, but I want the confirmation from a maintainer first

abondar commented 1 month ago

Hi!

I am not sure, but if understand correct - we already support that:

You can see example at https://github.com/tortoise/tortoise-orm/blob/develop/examples/relations_recursive.py#L24

Please write if I am wrong on what's issue

laggron42 commented 1 month ago

Hi, thanks for your response!

The example you linked shows recursdive relations, but they're not symmetrical. talks_to and gets_talked_to are two different sets despite being the same relation, if it's symmetrical then both sets should be identical.

Once again, I like the example of "If X is my friend, then I am the friend of X". If talks_to was symmetrical, this would work:

>>> await _1.talks_to.add(_2, _1_1_1, loose)
>>> await _1.talks_to.all()
[<Employee: 4>, <Employee: 6>, <Employee: 2>]
>>> await _2.talks_to.all()  # right now this returns an empty list
[<Employee: 3>]