I need to have a post associated to two users. The author and the moderator. I am trying without success this code
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
...
post = db.relationship('Post', foreign_keys=['posts.id'], backref='post_user', lazy='dynamic')
post_blame = db.relationship('Post', foreign_keys=['posts.moderated_by'], backref='post_blame', lazy='dynamic')
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
...
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
moderated_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
Error:
ArgumentError: Column-based expression object expected for argument 'foreign_keys'; got: 'posts.id', type <class 'str'>
One of the issues here is that you are currently trying to use table columns in the relationship foreign_keys, rather than class attributes.
That is, instead of using posts.id, you should be using Post.id. (In fact, to refer to a table column, you would need to use posts.c.id).
So, it is possible that your original code will work if you correct it to:
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
...
post = db.relationship('Post', foreign_keys='Post.id', backref='post_user', lazy='dynamic')
post_blame = db.relationship('Post', foreign_keys='Post.moderated_by', backref='post_blame', lazy='dynamic')
If it does not, then there several other options. First, you could establish these relationships in the Post class, where it is less ambiguous for sqlalchemy to find the foreign key relationship. Something like
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
...
author_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
moderated_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
post_user = db.relationship(User, foreign_keys=author_id, backref='post', lazy='dynamic')
post_blame = db.relationship(User, foreign_keys=moderated_by, backref='post_blame', lazy='dynamic')
Note in this version, we don't need to pass the foreign_keys value as a string, we can just refer directly to the column in scope.
Alternatively, if you wish to establish these relationships within User, you may you need to give sqlalchemy more information, by using primaryjoin ... perhaps something like:
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
...
post = db.relationship('Post', primaryjoin='User.id == Post.id', backref='post_user', lazy='dynamic')
post_blame = db.relationship('Post', foreign_keys='User.id == Post.moderated_by', backref='post_blame', lazy='dynamic')
Related
I'm using flask-sqlalchemy, this is not the first relations that i've built, but for some reason it gives me an error when i start flask:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'mapped class User->users'. Original exception was: 'Table' object has no attribute 'sender_id'
These are two models that i'm trying to connect via ForeignKeys:
User:
class User(UserMixin, db.Model):
__tablename__ = 'users'
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))
about_me = db.Column(db.String(140))
last_seen = db.Column(db.DateTime, default=datetime.utcnow)
#relations
posts = db.relationship('Post', back_populates='author', lazy='dynamic')
messages_sent = db.relationship('Message', foreign_keys='messages.sender_id',
back_populates='author', lazy='dynamic')
messages_received = db.relationship('Message', foreign_keys='messages.recipient_id',
back_populates='recipient', lazy='dynamic')
last_message_read_time = db.Column(db.DateTime)
followed = db.relationship(
'User', secondary=followers,
primaryjoin=(followers.c.follower_id == id),
secondaryjoin=(followers.c.followed_id == id),
backref=db.backref('followers', lazy='dynamic'), lazy='dynamic')
def __repr__(self):
return '<User {}>'.format(self.username)
And Messsage:
class Message(db.Model):
__tablename__ = 'messages'
id = db.Column(db.Integer, primary_key=True)
sender_id = db.Column(db.Integer, db.ForeignKey('users.id'))
recipient_id = db.Column(db.Integer, db.ForeignKey('users.id'))
body = db.Column(db.String(140))
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
#relations
author = db.relationship('User', back_populates='messages_sent', lazy='dynamic')
recipient = db.relationship('User', back_populates='messages_received', lazy='dynamic')
def __repr__(self):
return '<Message {}>'.format(self.body)
I've checked the database itself and it certainly has sender_id in the correct table, tried to change "foreign_keys" parameter to directly name of model Message.sender... Tried to change parameters of lazy on those relations. It still gives me same error.
This is subtle but messages in this case is a table so columns are referenced off of c, like messages.c.sender_id. To use the column of the model class (the mapped class) you would do Message.sender_id.
So...
messages_sent = db.relationship('Message', foreign_keys='messages.c.sender_id',
back_populates='author', lazy='dynamic')
# OR
messages_sent = db.relationship('Message', foreign_keys='Message.sender_id',
back_populates='author', lazy='dynamic')
There is some information here but it doesn't explain the table vs class situation: handling-multiple-join-paths I think I would just use ORM style references until you are more comfortable and then you could use table references if needed.
I have a example about one-to-many relationship in Flask-SQLAlchemy:
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(80))
email = db.Column(db.String(120), unique=True)
posts = db.relationship('Post', backref='user')
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey('user.id')
According to Declaring Models, I can get list of posts of User U by U.posts. But in constrast, how can I get author's name of Post P as an attribute like P.author_name, instead of P.user.name?
try P.user, according to what you defined in your backref
I can modify the Post class by:
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey('user.id')
#cached_property
def author_name(self):
return self.user.name
then, it will see P.author_name as P.user.name.
I'm designing the database for my own Q&A website, which is somewhat similar to Stack Overflow:
A "question" can have several "answers", but an "answer" has only one "question".
Both "questions" and "answers" can have several "comments", but a "comment" has only one "question" or "answer".
I have no idea how to design such a database. Here is what I've tried:
class Question(db.Model):
__tablename__ = 'questions'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.Unicode(64), unique=True)
body = db.Column(db.UnicodeText)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
answers = db.relationship('Answer', backref='question', lazy='dynamic')
class Answer(db.Model):
__tablename__ = 'answers'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.UnicodeText)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
question_id = db.Column(db.Integer, db.ForeignKey('questions.id'))
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
# post_id = db.Column(db.Integer, db.ForeignKey('')) ???
So you've already managed the first point.
What you're looking for is a generic relationship. You can find it in sqlalchemy_utils package.
from sqlalchemy_utils import generic_relationship
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
object_type = db.Column(db.Unicode(255))
object_id = db.Column(db.Integer)
object = generic_relationship(object_type, object_id)
Docs for generic relationship
So basically what it does, it stores the object_type as answer or question and object_id as object's primary key.
I suggest you extract a base class for Question and Answer, e.g. Post, and make Comment relate to Post, such that a Post can have multiple Comments.
SQLAlchemy ORM supports few strategies to implement inheritance in the database, and the right strategy to choose depends on the way you plan to query your entities. Here's the detailed documentation on how to properly configure it.
So you'd get something like this:
(disclaimer: I have not directly ran this code, but composed it from your example, and my own code. If it ain't working for you, let me know and I'll fix)
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
kind = Column(db.Unicode(64), nullable=False)
body = db.Column(db.UnicodeText)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
comments = db.relationship('Comment', backref='post', lazy='dynamic')
__mapper_args__ = {
'polymorphic_identity': 'posts',
'polymorphic_on': kind
}
class Question(Post):
__tablename__ = 'questions'
title = db.Column(db.Unicode(64), unique=True)
answers = db.relationship('Answer', backref='question', lazy='dynamic')
__mapper_args__ = {
'polymorphic_identity': 'questions',
}
class Answer(Post):
__tablename__ = 'answers'
question_id = db.Column(db.Integer, db.ForeignKey('questions.id'))
__mapper_args__ = {
'polymorphic_identity': 'answers',
}
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
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
I have a 3 tables: users, posts and comments. I'm trying to get username of comment author.
This is my models.py:
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.Integer, unique=True, index=True)
comments = db.relationship('Comment', backref='author', lazy='dynamic')
#i'm trying to:
comment_author = db.relationship('Comment', backref='comment_author_username', lazy='dynamic')
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(32), index=True)
comments = db.relationship('Comment', backref='post', lazy='dynamic')
body = db.Column(db.Text)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
author_id = db.Column(db.Integer, db.ForeignKey('users.id'))
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
#im trying to do:
#comment_author_username = db.Column(db.String(64), db.ForeignKey('users.username'))
but getting an error:
AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship User.comments - 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.
If i'm using in template {{comment.author_id}} it works properly(shows comment author id), but {{comment.author_id.username}} shows nothing. How i can get comment author username?
You are making TWO relation from table User (comments, comment_author) to table Comment (That basically they are the same).
The author_id in table Post has a db.ForeignKey('users.id') but there is no refer to table Post in your User table.
Basically what you want is, There are some posts that they have their own author, and for each post there are some comments that they have also their authors. The relation between your Post and User is One-To-Many and the relation between your Post and Comment is also One-To-Many. The Relation between Comment and User is One-To-Many. By sqlalchemy, Your Tables will be like below:
class User(db.Model, UserMixin):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.Integer, unique=True, index=True)
posts = db.relationship('Post', backref='poster', lazy='dynamic')
comments = db.relationship('Comment', backref='commenter', lazy='dynamic')
class Post(db.Model):
__tablename__ = 'posts'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(32), index=True)
body = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
comments = db.relationship('Comment', backref='comment_on_post', lazy='dynamic')
class Comment(db.Model):
__tablename__ = 'comments'
id = db.Column(db.Integer, primary_key=True)
body = db.Column(db.Text)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
post_id = db.Column(db.Integer, db.ForeignKey('posts.id'))
and for adding data, you can do like:
s = db.session
user = User(username='Alex')
post = Post(title='First try', body='This is my first try!')
comment = Comment(body='This is a useful post!')
user.posts.append(post)
user.comments.append(comment)
post.comments.append(comment)
s.add(user)
s.add(post)
s.add(comment)
s.commit()
s.close()
and for retrieve data:
s = db.session
comments = models.Comment.query.all()
for c in comments:
print c.user_id
s.close()