Users cannot follow groups in Flask (SQLAlchemy) - python

Error:
sqlalchemy.orm.exc.UnmappedColumnError
sqlalchemy.orm.exc.UnmappedColumnError: Can't execute sync rule for source column 'user.id'; mapper 'mapped class Group->group' does not map this column. Try using an explicit `foreign_keys` collection which does not include destination column 'group_followers.group_followed_id' (or use a viewonly=True relation).
I'm trying to build a system that users can follow groups.
Here is the error parts of code:
group_followers = db.Table('group_followers',
db.Column('group_follower_id', db.Integer, db.ForeignKey('group.id')),
db.Column('group_followed_id', db.Integer, db.ForeignKey('group.id'))
)
class User(UserMixin, db.Model):
...
group_followed = db.relationship(
'Group', secondary = group_followers,
primaryjoin = (group_followers.c.group_follower_id == id),
secondaryjoin = (group_followers.c.group_followed_id == id),
backref = db.backref('group_followers', lazy='dynamic'), lazy='dynamic')
...
def group_follow(self, group):
if not self.is_following(group):
self.group_followed.append(group)

You need to clearly define the schema of your two tables.
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(64), index=True, unique=True)
email = db.Column(db.String(120), index=True, unique=True)
password_hash = db.Column(db.String(128))
groups = db.relationship('Group', backref='user', lazy='dynamic')
def __repr__(self):
return f'User {self.username}'
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
# You can add any other field you want
# ...
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
Above, I have shown how to create a relationship from the perspective of a user using db.relationship(). To make this relationship, you will need to add a ForeignKey() in the table you want to reference. user in db.ForeingKey(user.id) refers to the User table.
The association table you have created above (group_followers) is not part of any models so add it outside any of the classes User and Group:
group_followers = db.Table('group_followers',
db.Column('group_follower_id', db.Integer, db.ForeignKey('group.id')),
db.Column('group_followed_id', db.Integer, db.ForeignKey('group.id'))
)
Then, declare the many-to-many relationship in the User's table:
class User(UserMixin, db.Model):
...
group_followed = db.relationship(
'Group', secondary = group_followers,
primaryjoin = (group_followers.c.group_follower_id == id),
secondaryjoin = (group_followers.c.group_followed_id == id),
backref = db.backref('group_followers', lazy='dynamic'), lazy='dynamic')
def group_follow(self, group):
if not self.is_following(group):
self.group_followed.append(group)
Run your migrations to update and apply these changes:
(venv)$ flask db migrate -m 'group followers'
(venv)$ flask db upgrade
This should work for a user who wants to follow a group. You can define other functions to unfollow a group or check whether a user is already following a group.

Related

Flask SQLAlchemy: adding third column to joining table

Context: I'm making an auctioning website for which I am using Flask-SQLAlchemy. My tables will need to have a many-to-many relationship (as one artpiece can have many user bids and a user can bid on many artpieces)
My question is: it is possible to add another column to my joining table to contain the id of the user bidding, the id of artpiece that they are bidding on and also how much they bid? Also if yes, how would I include this bid in the table when I add a record to said table?
bid_table = db.Table("bid_table",
db.Column("user_id", db.Integer, db.ForeignKey("user.user_id")),
db.Column("item_id", db.Integer, db.ForeignKey("artpiece.item_id"))
)
class User(db.Model):
user_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
username = db.Column(db.Integer, unique=True, nullable=False)
email = db.Column(db.String(50), unique =True, nullable=False)
password = db.Column(db.String(60), nullable=False)
creation_date = db.Column(db.DateTime, default=str(datetime.datetime.now()))
bids = db.relationship("Artpiece", secondary=bid_table, backref=db.backref("bids", lazy="dynamic"))
class Artpiece(db.Model):
item_id = db.Column(db.Integer, unique=True, primary_key=True, nullable=False)
artist = db.Column(db.String(40), nullable=False)
buyer = db.Column(db.String(40), nullable=False)
end_date = db.Column(db.String(40))
highest_bid = db.Column(db.String(40))
It is possible to do this with SQL Alchemy, but it's very cumbersome in my opinion.
SQLAlchemy uses a concept called an Association Proxy to turn a normal table into an association table. This table can have whatever data fields you want on it, but you have to manually tell SQLAlchemy which columns are foreign keys to the other two tables in question.
This is a good example from the documentation.
In your case, the UserKeyword table is the association proxy table that you want to build for your user/bid scenario.
The special_key column is the arbitrary data you would store like the bid amount.
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import backref, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column(Integer, primary_key=True)
name = Column(String(64))
# association proxy of "user_keywords" collection
# to "keyword" attribute
keywords = association_proxy('user_keywords', 'keyword')
def __init__(self, name):
self.name = name
class UserKeyword(Base):
__tablename__ = 'user_keyword'
user_id = Column(Integer, ForeignKey('user.id'), primary_key=True)
keyword_id = Column(Integer, ForeignKey('keyword.id'), primary_key=True)
special_key = Column(String(50))
# bidirectional attribute/collection of "user"/"user_keywords"
user = relationship(User,
backref=backref("user_keywords",
cascade="all, delete-orphan")
)
# reference to the "Keyword" object
keyword = relationship("Keyword")
def __init__(self, keyword=None, user=None, special_key=None):
self.user = user
self.keyword = keyword
self.special_key = special_key
class Keyword(Base):
__tablename__ = 'keyword'
id = Column(Integer, primary_key=True)
keyword = Column('keyword', String(64))
def __init__(self, keyword):
self.keyword = keyword
def __repr__(self):
return 'Keyword(%s)' % repr(self.keyword)
Check out the full documentation for instructions on how to access and create this kind of model.
Having used this in a real project, it's not particularly fun and if you can avoid it, I would recommend it.
https://docs.sqlalchemy.org/en/14/orm/extensions/associationproxy.html

Stuck with Flask-Sqlalchemy Relationship system

I want to create database, that consist user info(sqlite db)
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
login = db.Column(db.String(10), unique = True)
email = db.Column(db.String(20), unique = True)
psw = db.Column(db.String(500), nullable=True)
def __repr__(self):
return f"<User {self.id}>"
This is what i already did.
I want to create fields upcoming_friends, incoming_friends, friends, i think that i need to create a new class that will extends user , but I did not find the documentation and don't understand how to do it.
The User table stores the information that you need about a particular user. If you want to find out what friends this user might have in your application, then you can create another table called Friends.
class Friend(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
upcoming_friends = db.Column(db.String(64), unique = True)
incoming_friends = db.Column(db.String(64), unique = True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
def __repr__(self):
return f"<Friends: {self.id}>"
To create a relationship between these two database structures, we will do as follows:
class User(db.Model, UserMixin):
id = db.Column(db.Integer, primary_key=True)
login = db.Column(db.String(10), unique = True)
email = db.Column(db.String(20), unique = True)
friends = db.relationship('Friend', backref='<give-a-reference>', lazy='dynamic')
def __repr__(self):
return f"<User {self.id}>"
The user_id field was initialized as a foreign key to user.id, which means that it references an id value from the user's table. In this reference the user part is the name of the database table for the model.
There is a bit of inconsistency when it comes to referring to the user table in db.ForeignKey. Here, you can see that the user table starts with a lower case, whereas when it comes to referencing Friend table in db.relationship we begin with an upper case.

SqlAlchemy returns wrong data for association table with association proxy

I defined a role / permission model using the following model structure:
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), unique=True, nullable=False)
permissions = db.relationship("Permission", secondary=Role2Permission.__tablename__)
#classmethod
def find_by_id(cls, id):
return cls.query.filter_by(id=id).first()
class Role2Permission(db.Model):
__tablename__ = 'role_2_permission'
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'), primary_key=True)
permission_id = db.Column(db.Integer, db.ForeignKey('permissions.id'), primary_key=True)
bool_value = db.Column(db.Boolean, default=False)
class Permission(db.Model):
__tablename__ = 'permissions'
id = db.Column(db.Integer, primary_key=True)
action = db.Column(db.String(255))
role2permission = db.relationship('Role2Permission', lazy='joined')
bool_value = association_proxy('role2permission', 'bool_value')
When I fetch a role I want to have the permission value (bool_value) to be set via the association proxy coming from the association table. However using cls.query.filter_by(id=id).first() (where cls is the Role) returns the wrong bool_values for the queried role. I think the reason can be seen when looking at the generated SQL:
SELECT permissions.id AS permissions_id,
permissions.action AS permissions_action,
role_2_permission_1.role_id AS role_2_permission_1_role_id,
role_2_permission_1.permission_id AS role_2_permission_1_permission_id,
role_2_permission_1.bool_value AS role_2_permission_1_bool_value
FROM role_2_permission,
permissions
LEFT OUTER JOIN role_2_permission AS role_2_permission_1 ON permissions.id = role_2_permission_1.permission_id
WHERE 1 = role_2_permission.role_id AND permissions.id = role_2_permission.permission_id
I think this is fetching too many rows because it's selecting from the permissions table instead of just joining it to the role_2_permission table but then for some reason joining role_2_permission again. Somehow flask / sqlalchemy is then reducing the returned rows in a bad way: It's not actually so instead of the bool_values that belong to e.g. role 1, it returns the bool_values that belong to role 2.
How do I have to fix my model to get the correct permission data when querying the role?

How to set one to many and one to one relationship at same time in Flask-SQLAlchemy?

I'm trying to create one-to-one and one-to-many relationship at the same time in Flask-SQLAlchemy. I want to achieve this:
"A group has many members and one administrator."
Here is what I did:
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(140), index=True, unique=True)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, server_default=db.func.now())
members = db.relationship('User', backref='group')
admin = db.relationship('User', backref='admin_group', uselist=False)
def __repr__(self):
return '<Group %r>' % (self.name)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
admin_group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
created_at = db.Column(db.DateTime, server_default=db.func.now())
However I got an error:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join
condition between parent/child tables on relationship Group.members -
there are multiple foreign key paths linking the tables. Specify the
'foreign_keys' argument, providing a list of those columns which
should be counted as containing a foreign key reference to the parent
table.
Does anyone know how to do that properly?
The solution is to specify the foreign_keys argument on all relationships:
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
group_id = Column(Integer, ForeignKey('groups.id'))
admin_group_id = Column(Integer, ForeignKey('groups.id'))
class Group(Base):
__tablename__ = 'groups'
id = Column(Integer, primary_key=True)
members = relationship('User', backref='group', foreign_keys=[User.group_id])
admin = relationship('User', backref='admin_group', uselist=False, foreign_keys=[User.admin_group_id])
Perhaps consider the admin relation in the other direction to implement "a group has many members and one admin":
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
group_id = Column(Integer, ForeignKey('groups.id'))
group = relationship('Group', foreign_keys=[group_id], back_populates='members')
class Group(Base):
__tablename__ = 'groups'
id = Column(Integer, primary_key=True)
members = relationship('User', foreign_keys=[User.group_id], back_populates='group')
admin_user_id = Column(Integer, ForeignKey('users.id'))
admin = relationship('User', foreign_keys=[admin_user_id], post_update=True)
See note on post_update in the documentation. It is necessary when two models are mutually dependent, referencing each other.
The problem you're getting comes from the fact that you've defined two links between your classes - a User has a group_id (which is a Foreign Key), and a Group has an admin (which is also defined by a Foreign Key). If you remove the Foreign Key from the admin field the connection is no longer ambiguous and the relationship works. This is my solution to your problem (making the link one-to-one):
from app import db,app
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(140), index=True, unique=True)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, server_default=db.func.now())
admin_id = db.Column(db.Integer) #, db.ForeignKey('user.id'))
members = db.relationship('User', backref='group')
def admin(self):
return User.query.filter_by(id=self.admin_id).first()
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80), unique=True)
group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
created_at = db.Column(db.DateTime, server_default=db.func.now())
The one drawback to this is that the group object doesn't have a neat admin member object you can just use - you have to call the function group.admin() to retrieve the administrator. However, the group can have many members, but only one of them can be the administrator. Obviously there is no DB-level checking to ensure that the administrator is actually a member of the group, but you could add that check into a setter function - perhaps something like:
# setter method
def admin(self, user):
if user.group_id == self.id:
self.admin_id = user.id
# getter method
def admin(self):
return User.query.filter_by(id=self.admin_id).first()
Ok, I found a workaround for this problem finally. The many-to-many relationship can coexist with one-to-many relationship between the same two tables at the same time.
Here is the code:
groups_admins = db.Table('groups_admins',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('group_id', db.Integer, db.ForeignKey('group.id'))
)
class Group(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(140), index=True, unique=True)
description = db.Column(db.Text)
created_at = db.Column(db.DateTime, server_default=db.func.now())
members = db.relationship('User', backref='group')
admins = db.relationship('User',
secondary=groups_admins,
backref=db.backref('mod_groups', lazy='dynamic'),
lazy='dynamic')
def __repr__(self):
return '<Group %r>' % (self.name)
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
group_id = db.Column(db.Integer, db.ForeignKey('group.id'))
created_at = db.Column(db.DateTime, server_default=db.func.now())
I still want someone to tell me how to set one-to-many and one-to-one relationship at the same time, so I leave my answer here and won't accept it forever.
This link solved it for me
most important thing is to specify foreign_keys value in the relation as well as the primary join

flask-migrate wants to drop my indeces

I've got a Flask app with the following models:
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(64), unique=True, index=True)
password_hash = db.Column(db.String(128))
city_id = db.Column(db.Integer, db.ForeignKey('cities.id'))
class City(db.Model):
__tablename__ = 'cities'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(255), nullable=False)
user_ids = db.relationship('User', backref='city', lazy='dynamic')
I've run a migration to specify my indices and foreign key constraints:
def upgrade():
op.create_foreign_key('fk_user_city', "users", "cities", ["city_id"], ["id"])
op.create_index('city_idx', 'users', ['city_id'])
However, any time I create another new migration Alembic seems to want to drop my indexes.
Is there a way to freeze Alembic's autogeneration at the current DB/Model schema?
Check this page. You will need to change env.py under migrations folder.
EnvironmentContext.configure.include_object
or
EnvironmentContext.configure.include_schemas
should be what you are looking for.

Categories

Resources