SQLAlchemy different tables for relationship? - python

Suppose I have the following tables/relationships defined:
class Post(Base):
__table_name__ = "post"
id = Column(Integer, primary_key=True)
text = Column(String(100))
class Comment(Base):
__table_name__ = "comment"
id = Column(Integer, primary_key=True)
post_id = Column(Integer, ForeignKey("post.id"), nullable=False)
text = Column(String(100))
Now, I want to have notifications of events, like "You were tagged in a comment" or "you were tagged in a post". Is there some way to have a foreign key relationship in SQLAlchemy that can point to either a comment or a post (or several other tables in reality)? Something like:
class Notification(Base):
__table_name__ = "notification"
id = Column(Integer, primary_key=True)
target = relationship(??either post or comment??)
user_id = Column(Integer, ForeignKey("users.id")
created_date = Column(Datetime, default=datetime.utcnow)
I suppose you could just put foreign keys to all the different types and make all but one null, but that seems ugly. I'd also rather not have multiple tables for each type of notification; as in comment_notification and post_notification. Any ideas?

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

How to relate APScheduler JobStore with SQLAlchemy Models (foreign key) - Python Flask

I've got a Python Flask app using flask.ext.sqlalchemy and apscheduler.schedulers.background. I've created a JobStore and gotten a table called apscheduler_jobs is has the following fields:
|id |next_run_time|job_state|
------------------------------
|TEXT| REAL | TEXT |
I want to relate a an SQLAlchemy Model object to that table using something like this:
from apscheduler.schedulers.background import BackgroundScheduler
scheduler = BackgroundScheduler()
scheduler.add_jobstore('sqlalchemy', url=app.config['SQLALCHEMY_DATABASE_URI'])
class Event(db.Model):
__tablename__ = "event"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), nullable=False)
jobs = db.relationship('scheduler', backref='apscheduler_jobs')
So I want to use the table from the APScheduler apscheduler_jobs and then associate that with a foreign key to my Event object. That last line there will basically break as "scheduler" isn't a defined SQLAlchmey model
qlalchemy.exc.InvalidRequestError: When initializing mapper Mapper|Event|event, expression 'scheduler' failed to locate a name ("name 'scheduler' is not defined"). If this is a class name, consider adding this relationship() to the <class 'project.models.Event'> class after both dependent classes have been defined.
So I think I need an inbetween Model class called "job" or something, then relate that to apscheduler_jobs, but something here still feels bad - because APScheduler is making this table up I've got no control over what's going on there - should I be concerned about that?
EDIT1:
So I created 2 models, one "Event" then one "Job", the "Job" then relates to the table apscheduler_jobs
class Job(db.Model):
__tablename__ = "job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), nullable=False)
apscheduler_job_id = db.Column(db.Integer, db.ForeignKey('apscheduler_jobs.id'))
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
problem there is that when I dropped the DB and recreated it it's thrown the error:
sqlalchemy.exc.NoReferencedTableError: Foreign key associated with column 'job.apscheduler_job_id' could not find table 'apscheduler_jobs' with which to generate a foreign key to target column 'id'
Now I could get around that in my database creation script, but again it still feels like I'm doing this the wrong way
EDIT2
I managed to get it to work, though this feels pretty wrong, I've now got 3 models: Event, Job, and APSchedulerJobsTable. The final model basically matches what the APScheduler apscheduler_jobs looks like. There must be a better way to do this though.
from project import db
class Event(db.Model):
__tablename__ = "event"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), nullable=False)
jobs = db.relationship('Job', backref='job_event')
class Job(db.Model):
__tablename__ = "job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
apscheduler_job_id = db.Column(db.TEXT, db.ForeignKey('apscheduler_jobs.id'))
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
class APSchedulerJobsTable(db.Model):
# TODO: This feels bad man
__tablename__ = "apscheduler_jobs"
id = db.Column(db.TEXT, primary_key=True, autoincrement=True)
next_run_time = db.Column(db.REAL)
job_state = db.Column(db.TEXT)
Ok, two solutions - neither really perfect IMO:
Solution One, probably more clean - simply have a Text field in the job table that contains aspscheduler_job_ids - this is not a foreign key though but once the aspscheduler_job ID is known it's possible to go ahead and store it in the job table for later reference
class Event(db.Model):
__tablename__ = "event"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), nullable=False)
jobs = db.relationship('Job', backref='job_event')
class Job(db.Model):
__tablename__ = "job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
apscheduler_job_id = db.Column(db.TEXT)
Catch for this one is in order to drop the full db you'll need to run this to include dropping the unmanaged table apscheduler_jobs:
db.reflect()
db.drop_all()
Solution Two, add the apscheduler table to the model itself, and then set up the foreign key:
class Event(db.Model):
__tablename__ = "event"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
name = db.Column(db.String(255), nullable=False)
jobs = db.relationship('Job', backref='job_event')
class Job(db.Model):
__tablename__ = "job"
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
event_id = db.Column(db.Integer, db.ForeignKey('event.id'))
apscheduler_job_id = db.Column(db.TEXT, db.ForeignKey('apscheduler_jobs.id'))
class APSchedulerJobsTable(db.Model):
# TODO: This feels bad man
__tablename__ = "apscheduler_jobs"
id = db.Column(db.TEXT, primary_key=True, autoincrement=True)
next_run_time = db.Column(db.REAL)
job_state = db.Column(db.TEXT)
job = db.relationship('Job', backref='job_event')

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

Generated queries contain redundant products?

I have rather simple models like these:
TableA2TableB = Table('TableA2TableB', Base.metadata,
Column('tablea_id', BigInteger, ForeignKey('TableA.id')),
Column('tableb_id', Integer, ForeignKey('TableB.id')))
class TableA(Base):
__tablename__ = 'TableA'
id = Column(BigInteger, primary_key=True)
infohash = Column(String, unique=True)
url = Column(String)
tablebs = relationship('TableB', secondary=TableA2TableB, backref='tableas')
class TableB(Base):
__tablename__ = 'TableB'
id = Column(Integer, primary_key=True)
url = Column(String, unique=True)
However, sqla generates queries like
SELECT "TableB".id, "TableB".url AS "TableB_url" FROM "TableB", "TableA2TableB"
WHERE "TableA2TableB".tableb_id = "TableB".id AND "TableA2TableB".tablea_id = 408997;
But why is there a cartesian product in the query when the attributes selected are those in TableB? TableA2TableB shouldn't be needed.
Thanks
As it is right now, there is a backref relationship in TableB (tableas) and it's loaded because the default loading mode is set to select.
You may want to change the TableA.tablebs to
tablebs = relationship('TableB', secondary=TableA2TableB, backref='tableas', lazy="dynamic")

sqlalchemy constraint in models inheritance

I have two simple models:
class Message(Backend.instance().get_base()):
__tablename__ = 'messages'
id = Column(Integer, primary_key=True, autoincrement=True)
sender_id = Column(Integer, ForeignKey('users.id'))
content = Column(String, nullable=False)
class ChatMessage(Message):
__tablename__ = 'chat_messages'
id = Column(Integer, ForeignKey('messages.id'), primary_key=True)
receiver_id = Column(Integer, ForeignKey('users.id'))
How to define constraint sender_id!=receiver_id?
This doesn't seem to work with joined table inheritance, I've tried and it complains that the column sender_id from Message doesn't exist when creating the constraint in ChatMessage.
This complaint makes sense, since sender_id wouldn't be in the same table as receiver_id when the tables are created, so the foreign key relationship would need to be followed to check the constraint.
One option is to make ChatMessage a single table.
Use CheckConstraint, placed in table args.
class ChatMessage(Base):
__tablename__ = 'chat_messages'
id = sa.Column(sa.Integer, primary_key=True)
sender_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
receiver_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
content = sa.Column(sa.String, nullable=False)
__table_args__ = (
sa.CheckConstraint(receiver_id != sender_id),
)

Categories

Resources