简体   繁体   中英

Flask-SQLAlchemy How to filter two different models based on one to many relationship?

I'm so sorry, there are a lot of question about this issue, but I just can't do it. I've been trying for literally two days.

I have three models:

class Post(db.Model):
    ### other columns
    oylar = db.relationship('Vote',backref='post_oylar',lazy='dynamic', cascade="all, delete")

class Plan(db.Model):
    ### other columns
    oylar = db.relationship('Vote',backref='oylar',lazy='dynamic', cascade="all, delete")

class Vote(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    post_id = db.Column(db.Integer, db.ForeignKey("post.id"))
    plani_id = db.Column(db.Integer, db.ForeignKey("plan.id"))

I need to query Plan and Post based on Vote user_id.

Let's say I have this A user with Id 1. Basically, I want to see which Plan and Post this user voted for.

I've tried so many queries that I can't even think of which one I should write here. But in order to be more clear, let me show you what I mean with this example:

post = db.session.query(Post).join(Vote).filter(Vote.user_id == 1)
plan = db.session.query(Plan).join(Vote).filter(Vote.user_id == 1)
final_query = #there should be something to merge these two queries.

It's been a nightmare for me. I don't know SQL syntax and I started learning with SQL-Alchemy, so please don't shoot.

Thanks for your time in advance.

Update : Solution that fits me best is using single-table inheritance. It has made my life easier.

You use a many-to-many relationship with an association object .
I think the models are okay, but I've restructured them a bit.

class Vote(db.Model):
    __tablename__ = 'votes'

    id = db.Column(db.Integer, primary_key=True)

    # Foreign Keys
    plan_id = db.Column(db.Integer, db.ForeignKey('plans.id'))
    post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
    user_id = db.Column(db.Integer, db.ForeignKey('users.id'))

    # Relationships
    # If a user, post or plan is deleted, referencing votes are also removed.
    # The associated plans and posts are loaded with the vote using a JOIN statement.
    plan = db.relationship('Plan',
        backref=db.backref('votes', cascade='all, delete-orphan'),
        lazy='joined')
    post = db.relationship('Post',
        backref=db.backref('votes', cascade='all, delete-orphan'),
        lazy='joined')
    user = db.relationship('User',
        backref=db.backref('votes', cascade='all, delete-orphan'))

    def __repr__(self):
        return f'Vote(plan_id={self.plan_id}, post_id={self.post_id}, user_id={self.user_id})'

class Plan(db.Model):
    __tablename__ = 'plans'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(255), index=True, nullable=False)

    def __repr__(self):
        return f'Plan(name={self.name})'

class Post(db.Model):
    __tablename__ = 'posts'
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(255), index=True, nullable=False)

    def __repr__(self):
        return f'Post(title={self.title})'

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64), index=True, nullable=False)

    # All plans and posts that have been voted for can be reached via jointable.
    # CAUTION, objects can be added to the lists, but because of the viewonly flag
    # they are not transferred to the database during a commit.
    # An inconsistent state is therefore possible.
    #
    # _voted_plans = db.relationship(
    #     'Plan',
    #     secondary='votes',
    #     backref=db.backref('users_voted', viewonly=True),
    #     viewonly=True
    # )
    #
    # _voted_posts = db.relationship(
    #     'Post',
    #     secondary='votes',
    #     backref=db.backref('users_voted', viewonly=True),
    #     viewonly=True
    # )

    def __repr__(self):
        return f'User(name={self.name})'

On the one hand, you can use the ORM method using the virtual relationships to list all the associated models. In this case, your table votes acts as a jointable and the associated objects of the classes Post and Plan can be queried directly via the relationship.

plan_post_pairs = [(vote.plan, vote.post) for vote in Vote.query.filter_by(user_id=user_id).all()]

As an alternative, you can also write your own request. As an example I'll give you both a SQL SELECT statement and a JOIN statement. I also ask for the identifier of the vote to list duplicate votes by a user on the same plan-post combinations.

# SELECT stmt
items = db.session.query(       # SELECT ... FROM ...
    Vote.id, Plan, Post
).filter(                       # WHERE ...
    Vote.plan_id == Plan.id,    # ... AND
    Vote.post_id == Post.id,    # ... AND
    Vote.user_id == user_id     # ...
).all()
# JOIN stmt
items = db.session.query(Vote.id, Plan, Post)\      # SELECT ...
    .select_from(Vote)\                             # FROM ...
    .outerjoin(Plan, Post)\                         # LEFT OUTER JOIN ... ON ...
    .filter(Vote.user_id == user_id)\               # WHERE ...
    .all()

The following example is a little more advanced and may help you in the future. All plan-post combinations of a user are requested including the number of votes for the respective pair from this user.

subquery = db.session\
    .query(Vote.plan_id, Vote.post_id, db.func.count('*').label('count'))\
    .group_by(Vote.plan_id, Vote.post_id)\
    .filter(Vote.user_id == user_id)\
    .subquery()

items = db.session\
    .query(Plan, Post, subquery.c.count)\
    .select_from(subquery)\
    .outerjoin(Plan, subquery.c.plan_id == Plan.id)\
    .outerjoin(Post, subquery.c.post_id == Post.id)\
    .all()

Because you're creating relationships to your Vote model, I would suggest you also establish a relationship between Vote and User :

# Modify your class to include the following line
class Vote(db.Model):
    ...
    user_id = db.Column(db.Integer, db.ForeignKey("user.id"))
    user = db.Relationship("User", backref="vote") #  <--- This assumes you have a `User` model
    ...

Now that that relationship is established, I would access these child objects from the user-vote relationship:

# 1. Get your user
my_user_id = 1
my_user = db.session.query(Post).get(my_user_id)

# Get the vote for this user --> this will return a list
# Note: there may be many votes, I'll use the first index as a demonstrative example.
my_votes = my_user.vote

# If there aren't any votes
if not my_votes:
    raise Exception(f'User id={my_user.id} has no votes')

# 2. Get the first vote 
my_vote = my_votes[0]

# 3. Get the children from that vote
# These are the keywords specified in the 'backref' parameter
my_post = my_vote.post_oylar
my_plan = my_vote.oylar

# Finally, print them
print('post:', my_post.__dict__)
print('plan:', my_plan.__dict__)

As a side note, I'd suggest you do some additional research on how to best establish relationships in your ORM models – it looks like that might be the source of confusion.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM