Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
216 views
in Technique[技术] by (71.8m points)

python - How to filter exact many to many

I have User and Room model in Flask SQLAlchemy. I need to filter if Room exists with users [user1, user2, ...]. Filter must be exact.
Here are my models:

room_users_table = db.Table(
  'room_users',
    db.metadata,
    db.Column('user', db.Integer, db.ForeignKey('user.id')),
    db.Column('room', db.Integer, db.ForeignKey('room.id'))
)

class User(db.Model):
    __tablename__ = 'user'
    id = db.Column(db.Integer, primary_key=True)
    first_name = db.Column(db.String(80))
    last_name = db.Column(db.String(80))
    password = db.Column(db.String(80))
    email = db.Column(db.String(120), unique=True)
    rooms = db.relationship(
        "Room",
        secondary=room_users_table,
        back_populates="users"
    )

class Room(db.Model):
   __tablename__ = 'room'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(80), unique=True)
    users = db.relationship(
        "User",
        secondary=room_users_table,
        back_populates="rooms"
    )

Thanks, I've searched all simular questions and can't find answer.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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()

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...