How to filter exact many to many

二次信任 提交于 2019-12-02 08:20:09

You can achieve this using a version of relational division and some additional filtering:

First build a temporary "table" (a union) of the emails you want to search against:

In [46]: emails = ['email1@mail.com', 'email2@mail.com']

In [47]: emails_union = db.union(*(db.select([db.literal(email).label('email')])
                                   for email in emails)).alias()

That may look a bit unwelcoming, but it essentially forms an SQL UNION like this:

SELECT 'email1@mail.com' AS email
UNION
SELECT 'email2@mail.com' AS email

and gives it an alias. Some databases may support other means to generate a new relation from a list, for example with Postgresql you could:

In [64]: from sqlalchemy.dialects.postgresql import array

In [65]: emails_relation = db.func.unnest(array(emails)).alias()

The division itself is done using a double negation, or 2 nested NOT EXISTS conditions:

In [48]: db.session.query(Room).\
    ...:     filter(~db.session.query().select_from(emails_union).
    ...:                filter(~Room.users.any(email=emails_union.c.email)).
    ...:                exists(),
    ...:            ~Room.users.any(User.email.notin_(emails))).\
    ...:     all()
Out[48]: [<__main__.Room at 0x7fad4d238128>]

In [49]: [(r.name, [u.email for u in r.users]) for r in _]
Out[49]: [('room1', ['email1@mail.com', 'email2@mail.com'])]

The query pretty much answers the question "find those Rooms for which no such email exists that is not in Room.users" – which finds rooms with all given emails – and then it applies the 3rd NOT EXISTS condition, which filters out rooms with additional emails. Without it the query would also return room2, which has emails 1, 2, and 3.

The searches were done against this data:

In [10]: users = [User(id=id_, email='email{}@mail.com'.format(id_))
    ...:          for id_ in range(1, 10)]

In [11]: rooms = [Room(id=id_, name='room{}'.format(id_))
    ...:          for id_ in range(1, 10)]

In [18]: db.session.add_all(users)

In [19]: db.session.add_all(rooms)

In [20]: for room, user1, user2 in zip(rooms, users, users[1:]):
    ...:     room.users.append(user1)
    ...:     room.users.append(user2)
    ...:     

In [21]: rooms[1].users.append(users[0])

In [22]: db.session.commit()

After two days of research I have a temporary solution.

-- room_users is the "secondary" table in 
   -- sqlalchemy many to many between User and Room
select room.name, room_users.room 
    from room_users join room on room_users.room = room.id 
    where room_users.user in (1,2) 
    group by room_users.room having count(*) = 2;

In python it will be something like this:

# members contains user emails
members = ['email1@mail.com', 'email2@mail.com']
db.session.query(Room.name).join(Room.users).filter(
    User.email.in_(members)
).group_by(Room.id).having(
    func.count(Room.id) == len(members)
).first()
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!