SQLAlchemy polymorphic Many-to-Many-Relation with Association Object - sqlalchemy

I have items of different type, say City, Country and Continent. And I have Labels. I want to be able to add multiple Labels to either City, Country and Continent objects. And each Label could have multiple items, too.
I cannot use table inheritance for defining City, Country and Continent here, since the models and their respective database tables are already existing and populated. Migrating the database to a different structure is impossible in my situation due to the size of the database. So I tried to make it with an Association Object, but I cannot figure out, how to create the relation from, say, Country to the Association in a way, that it only retrieves the lablables which have lablable_type = "country"
So, what I have looks like this:
Base = declarative_base()
class Lablable(Base):
__tablename__ = 'lablables'
label_id = db.Column(db.Integer, db.ForeignKey('labels.id'), primary_key=True)
lablable_id = db.Column(db.Integer)
lablable_type = db.Column(db.String(100))
labels = db.relationship("Label")
class Label(db.Model):
__tablename__ = "labels"
id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False)
caption = db.Column(db.String(200))
class Country(db.Model):
__tablename__ = "countries"
lablable_id = db.Column(db.Integer(), db.ForeignKey('lablables.id'))
lables = association_proxy('lablables', 'label',
creator=lambda x: Lablable(x=label))
How can I design my models to achieve what I want?

Related

joinedload and load_only but with filtering

I have two models with a simple FK relationship, Stock and Restriction (Restriction.stock_id FK to Stock).
class Restriction(Model):
__tablename__ = "restrictions"
id = db.Column(db.Integer, primary_key=True)
stock_id = FK("stocks.id", nullable=True)
name = db.Column(db.String(50), nullable=False)
class Stock(Model):
__tablename__ = "stocks"
id = db.Column(db.Integer, primary_key=True)
ticker = db.Column(db.String(50), nullable=False, index=True)
I would like to retrieve Restriction object and related Stock but only Stock's ticker (there are other fields omitted here). I can simply do this with:
from sqlalchemy.orm import *
my_query = Restriction.query.options(
joinedload(Restriction.stock).load_only(Stock.ticker)
)
r = my_query.first()
I get all columns for Restriction and only ticker for Stocks with above. I can see this in the SQL query run and also I can access r.stock.ticker and see no new queries run as it is loaded eagerly.
The problem is I cannot filter on stocks now, SQLAlchemy adds another FROM clause if I do my_query.filter(Stock.ticker == 'GME'). This means there is a cross product and it is not what I want.
On the other hand, I cannot load selected columns from relationship using join. ie.
Restriction.query.join(Restriction.stock).options(load_only(Restriction.stock))
does not seem to work with relationship property. How can I have a single filter and have it load selected columns from relationship table?
SQL I want run is:
SELECT restrictions.*, stocks.ticker
FROM restrictions LEFT OUTER JOIN stocks ON stocks.id = restrictions.stock_id
WHERE stocks.ticker = 'GME'
And I'd like to get a Restriction object back with its stock and stock's ticker. Is this possible?
joinedload basically should not be used with filter. You probably need to take contains_eager option.
from sqlalchemy.orm import *
my_query = Restriction.query.join(Restriction.stock).options(
contains_eager(Restriction.stock).load_only(Stock.ticker)
).filter(Stock.ticker == 'GME')
r = my_query.first()
Because you are joining using stock_id it will also be in the results as Stock.id beside Stock.ticker. But other fields would be omitted as you wish.
I have written short post about it recently if you are interested: https://jorzel.hashnode.dev/an-orm-can-bite-you

SQLAlchemy: Relationships based on third table

I'm trying to model following situation:
I need to set prices for groups of countries. So I have three entities:
class Zone:
zone = Column(Integer, primary_key=True)
class Country:
country = Column(String(2), primary_key=True)
zone = Column(Integer, ForeignKey('Zone.zone')
class Rate:
zone = Column(Integer, ForeignKey('Zone.zone')
rate = Column(Float)
Now I want to be able to access rates for a chosen Country. So I change my Country entity this way:
class Country:
country = Column(String(2), primary_key=True)
zone = Column(Integer, ForeignKey('Zone.zone')
rates = relationship('Rate', foreign_keys=[zone])
And I'm getting an error:
sqlalchemy.exc.NoForeignKeysError: Could not determine join condition between parent/child tables on relationship Country.rates - there are no foreign keys linking these tables. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or specify a 'primaryjoin' expression.
Ok, I changed the Country entity:
class Country:
country = Column(String(2), primary_key=True)
zone = Column(Integer, ForeignKey('Zone.zone')
rates = relationship('Rate', primaryjoin="Country.zone == Rate.zone")
The error is changed:
sqlalchemy.exc.ArgumentError: Could not locate any relevant foreign key columns for primary join condition 'countries.zone = rates.zone' on relationship Country.rates. Ensure that referencing columns are associated with a ForeignKey or ForeignKeyConstraint, or are annotated in the join condition with the foreign() annotation.
So the next version looks like that:
class Country:
country = Column(String(2), primary_key=True)
zone = Column(Integer, ForeignKey('Zone.zone')
rates = relationship('Rate', primaryjoin="foreign(Country.zone) == remote(Rate.zone)")
Surprisingly that partially worked. Partially because value of rates wasn't a list (by the way lazy='dynamic' caused an error as well) but a single (first) value. Somewhere I found a solution to that, which required change rather in Rate:
class Rate:
zone = Column(Integer, ForeignKey('Zone.zone')
rate = Column(Float)
__countries = relationship('Country',
primaryjoin="foreign(Rate.zone) == remote(Country.zone)",
backref='rates')
This finally worked. So here comes a question. The solution seems to be a bit cumbersome and doesn't rely on indexes, which might cause performance issues. Is there a better solution?

Optimizing hybrid_properties in SQLAlchemy

I have a piece of working code but it is very inefficient, instead of a single query with a join. I get one initial query, followed by one query per row in the response.
I have to following scenario:
class Job(Base, SerializeMixin, JobInterface):
__tablename__ = 'job_subjobs'
id = Column(Integer, primary_key=True, autoincrement=True)
group_id = Column(Integer, ForeignKey("job_groups.id"), nullable=False)
class Crash(Base, SerializeMixin):
__tablename__ = 'crashes'
id = Column(Integer, primary_key=True, autoincrement=True)
job_id = Column(Integer, ForeignKey("job_subjobs.id", ondelete='CASCADE'), nullable=False)
job = relationship('Job', backref='Crash')
#hybrid_property
def job_identifier(self):
return "{}:{}".format(self.job.group_id, self.job.id)
So given the above and I perform a query for all Crashes, It will perform one SELECT for all crashes. When I iterate and ask for job_identifier it will then do one separate SELECT for each crash.
self.session.query(Crash).all()
Is there someway i can create a #hybrid_property referencing a different table and have it JOIN from the beginning and preload the expression?
I've experimented with #xxx.expression without success. If all else fails I can add another foreign key in Crash table, but I would like to avoid changing current data structure if possible.
ended up using:
jobs = relationship('Job', backref='Crash', lazy='joined')

Two "one-to-one" references to same table in SQLAlchemy

Suppose I am modelling postal address changes. I'd like each AddressChange to have a before relationship to an Address, as well as an after relationship to another Address. And I'd like a reference back from the Address to the AddressChange it is associated with.
class AddressChange(Base):
__tablename__ = 'AddressChanges'
id = Column(Integer, primary_key=True)
before_id = Column(Integer, ForeignKey('Addresses.id'))
before = relationship('Address', foreign_keys=before_id, uselist=False,
back_populates='change')
after_id = Column(Integer, ForeignKey('Addresses.id'))
after = relationship('Address', foreign_keys=after_id, uselist=False,
back_populates='change')
class Address(Base):
__tablename__ = 'Addresses'
id = Column(Integer, primary_key=True)
street, city, etc = Column(String), Column(String), Column(String)
change = relationship('AddressChange')
However SQLAlchemy complains:
Could not determine join condition between parent/child tables on relationship Address.change - 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.
My Address does not have a foreign key reference to the parent table, and it's not clear to me why it should need one. If I add one, I get
Address.change and back-reference AddressChange.before are both of the same direction symbol('MANYTOONE'). Did you mean to set remote_side on the many-to-one side ?
Which is starting to get confusing, because the documentation for remote_side is for "self-referential relationships."
Thanks to #alex-grönholm for helping on #sqlalchemy.
This can be solved by adding a primaryjoin parameter to Address's side of the relationship to teach it how to map back to the parent AddressChange:
change = relationship('AddressChange', uselist=False,
viewonly=True,
primaryjoin=or_(
AddressChange.before_id == id,
AddressChange.after_id == id
))

SQLAlchemy recursive many-to-many relation

I've a case where I'm using one table to store user and group related datas. This column is called profile. So, basically this table is many-to-many table for the cases where one user is belonging in to many groups or there are many users in one group.
I'm a bit confused how it should be described...
Here's a simplified presentation of the class.
Entity relationship model
user_group_table = Table('user_group', metadata,
Column('user_id', Integer,ForeignKey('profiles.id',
onupdate="CASCADE", ondelete="CASCADE")),
Column('group_id', Integer, ForeignKey('profiles.id',
onupdate="CASCADE", ondelete="CASCADE"))
)
class Profile(Base)
__tablename__ = 'profiles'
id = Column(Integer, autoincrement=True, primary_key=True)
name = Column(Unicode(16), unique=True) # This can be either user- / groupname
groups = relationship('Profile', secondary=user_group_table, backref = 'users')
users = relationship('Profile', secondary=user_group_table, backref = 'groups')
#Example of the usage:
user = Profile()
user.name = 'Peter'
salesGroup = Profile()
salesGroup.name = 'Sales'
user.groups.append(salesGroup)
salesGroup.users
>[peter]
First of all, I agree with Raven's comment that you should use separate tables for Users and Groups. The reason being that you might get some inconsistent data where a User might have other Users as its users relations, as well as you might have cycles in the relationship tree.
Having said that, to make the relationship work declare it as following:
...
class Profile(Base):
__tablename__ = 'profiles'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(16), unique=True) # This can be either user- / groupname
groups = relationship('Profile',
secondary=user_group_table,
primaryjoin=user_group_table.c.user_id==id,
secondaryjoin=user_group_table.c.group_id==id,
backref='users')
...
Also see Specifying Alternate Join Conditions to relationship() documentation section.