What I am trying to do is have 2 foreign keys from User table inside Ban table here is how I did it:
class Ban(Base):
__tablename__ = "ban"
ban_id = Column(Integer, primary_key=True, index=True)
poll_owner_id = Column(Integer)
banned_by = Column(String , ForeignKey('user.username', ondelete='CASCADE', ), unique=True)
user_id = Column(Integer, ForeignKey('user.user_id', ondelete='CASCADE', ))
updated_at = Column(DateTime)
create_at = Column(DateTime)
ban_to_user = relationship("User", back_populates='user_to_ban', cascade='all, delete')
and User table:
class User(Base):
__tablename__ = "user"
user_id = Column(Integer, primary_key=True, index=True)
username = Column(String, unique=True)
email = Column(String)
create_at = Column(DateTime)
updated_at = Column(DateTime)
user_to_ban = relationship("Ban", back_populates='ban_to_user', cascade='all, delete')
When I try to run a query to fetch all users like this:
#router.get('/all')
async def get_all_users(db:Session = Depends(get_db)):
return db.query(models.User).all()
I get this error:
sqlalchemy.exc.InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Triggering mapper: 'mapped class User->user'. Origina
l exception was: Could not determine join condition between parent/child tables on relationship User.user_to_ban - 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.
I did the relationship between them as you can see but it states that there is problem between them. If needed I can show you how I did migration for my db using alembic if that is possible cause or is there a cleaner and better way to do this. Thanks in advance
You can have several foreign keys to a single table, like in your case for banned user and banned_by user.
You just need to disambiguate, which ForeignKey for which relationship (docs):
class Ban(Base):
__tablename__ = "ban"
id = Column(Integer, primary_key=True)
banned_user_id = Column(Integer, ForeignKey("user.id")) # for banned_user relationship
banned_by_user_id = Column(Integer, ForeignKey("user.id")) # for banned_by relationship
banned_user = relationship("User", foreign_keys=[banned_user_id], back_populates="bans")
banned_by = relationship("User", foreign_keys=[banned_by_user_id])
Full demo:
from sqlalchemy import (
Column,
ForeignKey,
Integer,
String,
create_engine,
select,
)
from sqlalchemy.orm import Session, declarative_base, relationship
Base = declarative_base()
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
username = Column(String, unique=True)
bans = relationship(
"Ban",
back_populates="banned_user",
foreign_keys="Ban.banned_user_id",
)
class Ban(Base):
__tablename__ = "ban"
id = Column(Integer, primary_key=True)
banned_user_id = Column(Integer, ForeignKey("user.id"))
banned_by_user_id = Column(Integer, ForeignKey("user.id"))
banned_user = relationship(
"User", foreign_keys=[banned_user_id], back_populates="bans"
)
banned_by = relationship("User", foreign_keys=[banned_by_user_id])
engine = create_engine("sqlite://", echo=True, future=True)
Base.metadata.create_all(engine)
spongebob = User(username="spongebob")
patrick = User(username="patrickstarr")
spongebob_bans_patrick = Ban(banned_by=spongebob, banned_user=patrick)
with Session(engine) as session:
session.add_all(
[
spongebob,
patrick,
spongebob_bans_patrick,
]
)
session.commit()
with Session(engine) as session:
result = session.scalars(select(Ban)).first()
print(
"User:",
result.banned_user.username,
"was banned by User:",
result.banned_by.username,
)
# User: patrickstarr was banned by User: spongebob
Related
In my pet project I need to send some article to user and log this action with ManyToMany relationship.
So, any article can be sent to any user, but only once. After sending we put data to 'sent_log' table with what article_id sent to which user_id.
How do I get (with SQLAlchemy ORM) all articles that is not sent to given user_id?
This code doesn't work for me.
def get_user_articles(db: Session, user_telegram_id: int):
"""Get user articles matching language code and user_telegram_id."""
return (
db.query(models.Article)
.select_from(join(left=models.Article, right=models.User))
.filter(models.Article.language_code == models.User.language_code)
.filter(models.User.telegram_id == user_telegram_id)
.all()
)
Maybe I've built bad DB structure, guide me than.
My models:
sent_log = Table('sent_log', Base.metadata,
Column('user_id', ForeignKey('users.id'), primary_key=True),
Column('article_id', ForeignKey('articles.id'), primary_key=True)
)
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
telegram_id = Column(Integer, unique=True, index=True)
username = Column(String(50))
pet_name = Column(String(50))
language_code = Column(String(5))
sent_articles = relationship("Article", secondary=sent_log, back_populates="sent_to_user")
class Article(Base):
__tablename__ = "articles"
id = Column(Integer, primary_key=True, index=True)
text = Column(String(1024))
image_url = Column(String(500))
language_code = Column(String(255), index=True)
sent_to_user = relationship("User", secondary=sent_log, back_populates="sent_articles")
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
I have two tables, Users and ChatSessions. ChatSessions has two fields, user_id and friend_id, both foreign keys to the Users table.
user_id always contains the user that initiated the chat session, friend_id is the other user. As a certain user can have chat sessions initiated by him, or his friends, he can have his id either as user_id or as friend_id, in various sessions.
Is it possible to define a relationship in the Users table, where i have access to all the chat_sessions of that user, no matter whether his id is in user_id or friend_id?
Something like this:
chat_sessions = db.relationship('chat_sessions',
primaryjoin="or_(User.id==ChatSession.user_id, User.id==ChatSession.friend_id)",
backref="user")
I receive the following error when I try to commit an entry to the Users table:
ERROR main.py:76 [10.0.2.2] Unhandled Exception [93e3f515-7dd6-4e8d-b096-8239313433f2]: relationship 'chat_sessions' expects a class or a mapper argument (received: <class 'sqlalchemy.sql.schema.Table'>)
The models:
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
email = db.Column(db.String(60), index=True, unique=True)
password = db.Column(db.String(255))
name = db.Column(db.String(100))
active = db.Column(db.Boolean(), nullable=False)
chat_sessions = db.relationship('chat_sessions',
primaryjoin="or_(User.id==ChatSession.user_id, User.id==ChatSession.friend_id)")
class ChatSession(db.Model):
__tablename__ = 'chat_sessions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
friend_id = db.Column(db.Integer, db.ForeignKey('users.id'))
status = db.Column(db.String(50))
user = db.relationship('User', foreign_keys=[user_id])
friend = db.relationship('User', foreign_keys=[friend_id])
It's difficult to be certain without seeing the tables' code, but it might be sufficient to remove the backref argument.
Here's a pure SQLAlchemy implementation that seems to do what you want:
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import orm
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
all_chats = orm.relationship('Chat',
primaryjoin="or_(User.id==Chat.user_id, User.id==Chat.friend_id)")
def __repr__(self):
return f'User(name={self.name})'
class Chat(Base):
__tablename__ = 'chats'
id = sa.Column(sa.Integer, primary_key=True)
user_id = sa.Column(sa.Integer, sa.ForeignKey('users.id'))
friend_id = sa.Column(sa.Integer, sa.ForeignKey('users.id'))
user = orm.relationship('User', foreign_keys=[user_id])
friend = orm.relationship('User', foreign_keys=[friend_id])
def __repr__(self):
return f'Chat(user={self.user.name}, friend={self.friend.name})'
engine = sa.create_engine('sqlite:///')
Base.metadata.create_all(bind=engine)
Session = orm.sessionmaker(bind=engine)
usernames = ['Alice', 'Bob', 'Carol']
session = Session()
users = [User(name=name) for name in usernames]
session.add_all(users)
session.flush()
a, b, c = users
session.add(Chat(user_id=a.id, friend_id=b.id))
session.add(Chat(user_id=a.id, friend_id=c.id))
session.add(Chat(user_id=c.id, friend_id=a.id))
session.commit()
session.close()
session = Session()
users = session.query(User)
for user in users:
for chat in user.all_chats:
print(user, chat)
print()
session.close()
This is the output:
User(name=Alice) Chat(user=Alice, friend=Bob)
User(name=Alice) Chat(user=Alice, friend=Carol)
User(name=Alice) Chat(user=Carol, friend=Alice)
User(name=Bob) Chat(user=Alice, friend=Bob)
User(name=Carol) Chat(user=Alice, friend=Carol)
User(name=Carol) Chat(user=Carol, friend=Alice)
I have two very simple models. In my Post model there are supposed to be two relationships into the User table. One is for the owner of the post and one is for the last editor of the post. They can be different values, but both refer to the same User table.
My models are set up like this
class Post(Base):
last_editor_id = Column(BigInteger, ForeignKey('users.id'), nullable=True)
last_editor = relationship('User', backref='posts', foreign_keys=[last_editor_id])
owner_id = Column(BigInteger, ForeignKey('users.id'), nullable=False, index=True)
owner = relationship('User', backref='posts', foreign_keys=[owner_id])
class User(Base):
'''This represents a user on the site'''
__tablename__ = 'users'
id = Column(BigInteger, primary_key=True, unique=True)
name = Column(BigInteger, nullable=False)
When I attempt to create these models though, I get the following error
sqlalchemy.exc.ArgumentError: Error creating backref 'posts' on relationship 'Post.owner': property of that name exists on mapper 'Mapper|User|users'
How do I correct this so that I can maintain both forgeign keys in the Post model?
The error is telling you that you've used post as a name more then once for your backrefs, all you need to do is give the backref's unique names. Here's a complete example-- I've added a id primary key to the Post class, and also some __repr__s so we get some readable output.
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, BigInteger, ForeignKey, Integer
from sqlalchemy.orm import relationship, sessionmaker
Base = declarative_base()
engine = create_engine('sqlite://') ## In Memory.
Session = sessionmaker()
Session.configure(bind=engine)
session = Session()
class Post(Base):
__tablename__ = 'post'
id = Column(Integer, primary_key=True)
last_editor_id = Column(BigInteger, ForeignKey('users.id'), nullable=True)
last_editor = relationship('User', backref='editor_posts', foreign_keys=[last_editor_id])
owner_id = Column(BigInteger, ForeignKey('users.id'), nullable=False, index=True)
owner = relationship('User', backref='owner_posts', foreign_keys=[owner_id])
def __repr__(self):
return '<Post: {}>'.format(self.id)
class User(Base):
'''This represents a user on the site'''
__tablename__ = 'users'
id = Column(BigInteger, primary_key=True, unique=True)
name = Column(BigInteger, nullable=False)
def __repr__(self):
return '<User: {}>'.format(self.name)
Base.metadata.create_all(engine)
bob = User(name='Bob', id=1)
alice = User(name='Alice', id=2)
post = Post(owner=alice, last_editor=bob, id=1)
session.add(post)
session.commit()
bob = session.query(User).get(1)
print bob
# <User: Bob>
print bob.editor_posts
# [<Post: 1>]
print bob.owner_posts
# []
post = session.query(Post).get(1)
print post.owner
# <User: Alice>
print post.last_editor
# <User: Bob>
Now when you query a user, you can ask that object user.owner_posts or user.editor_posts.
In general it's a naming Problem of the backref.
Since 1:n relationships are sometimes a bit confusing, I set the relationship attribute
always on the singular site, to avoid confusion.
then the backref name is always singular. and the relationship attribute is always in the Class where the foreignkey is referencing to.
Now to my suggestion for the fixed code:
class Post(Base):
last_editor_id = Column(BigInteger, ForeignKey('users.id'), nullable=True)
owner_id = Column(BigInteger, ForeignKey('users.id'), nullable=False, index=True)
class User(Base):
'''This represents a user on the site'''
__tablename__ = 'users'
id = Column(BigInteger, primary_key=True, unique=True)
name = Column(BigInteger, nullable=False)
owned_posts = relationship('Post', backref='owner')
edited_posts = relationship('Post', backref='last_editor')
Now you can get all the owned posts of a User with User.owned_posts and all owners of a post with Post.owner. Same with the last_edited attribute.
For additional info you could read the docs how to set up relationships
Im using sqlalchemy to design a forum style website. I started knocking out the design but everytime I try to test it with a few inserts, it dumps a brick;
NoForeignKeysError: Could not determine join condition between parent/child
tables on relationship Thread.replies - there are no foreign keys linking
these tables. Ensure that referencing columns are associated with a
ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.
Here are my "models"
from sqlalchemy import Integer, Column, String, create_engine, ForeignKey
from sqlalchemy.orm import relationship, sessionmaker, backref
from .database import Base # declarative base instance
class User(Base):
__tablename__ = "user"
id = Column(Integer, primary_key=True)
username = Column(String, unique=True)
email = Column(String, unique=True)
threads = relationship("Thread", backref="user")
posts = relationship("Post", backref="user")
class Post(Base):
__tablename__ = "post"
id = Column(Integer, primary_key=True)
name = Column(String)
body = Column(String)
author = Column(Integer, ForeignKey("user.id"))
class Thread(Base):
__tablename__ = "thread"
id = Column(Integer, primary_key=True)
name = Column(String)
desc = Column(String)
replies = relationship("Post", backref="thread")
author_id = Column(Integer, ForeignKey("user.id"))
board_id = Column(Integer, ForeignKey("board.id"))
class Board(Base):
__tablename__ = "board"
id = Column(Integer, primary_key=True)
name = Column(String)
desc = Column(String)
threads = relationship("Thread", backref="board")
category_id = Column(Integer, ForeignKey("category.id"))
class Category(Base):
__tablename__ = "category"
id = Column(Integer, primary_key=True)
name = Column(String)
desc = Column(String)
threads = relationship("Board", backref="category")
engine = create_engine('sqlite:///:memory:', echo=True)
Base.metadata.create_all(engine)
session_factory = sessionmaker(bind=engine)
session = session_factory()
Your Post model has no thread reference. Add a column to Post referencing the Thread a post belongs to:
class Post(Base):
__tablename__ = "post"
id = Column(Integer, primary_key=True)
name = Column(String)
body = Column(String)
author = Column(Integer, ForeignKey("user.id"))
thread_id = Column(Integer, ForeignKey('thread.id'))
We can't use the name thread because that's what the Post.replies relationship will add to retrieved Thread instances.
This is a One to Many relationship as documented in the SQLAlchemy Relationship Configuration documentation.
You should add a field in the Post model that reads:
thread_id = Column(Integer, ForeignKey("thread.id"), nullable=True, default=None)
How is SQLAlchemy supposed to know how this relationship you defined supposed to link a thhread to a post? That's why you should have a foreign key from a post to its thread. You can allow it to be null, if it does not belong to a thread, it depends on your use case.