nested relationship in hybrid_property in sqlalchemy - python

I have a #hybrid_property which references a nested relationship self.establishment_type.establishment_base_type.name == 'QSR'. It works on a Location object as in assert location.is_qsr == True, but not in a filter. I have tried adding a #is_qsr.expression function, but can't get any of them working. How can I enable a filter such as query(Location).filter(Location.is_qsr == True)?
class Location(Base):
__tablename__ = 'houses'
id = Column(Integer, primary_key=True)
establishment_type_id = Column(
Integer, ForeignKey('establishment_types.id')
)
establishment_type = relationship('EstablishmentType')
#hybrid_property
def is_qsr(self):
if self.establishment_type:
if self.establishment_type.establishment_base_type:
return self.establishment_type.establishment_base_type.name == 'QSR'
return False
class EstablishmentType(Base):
__tablename__ = 'establishment_types'
id = Column(Integer, primary_key=True)
establishment_base_type_id = Column(
Integer, ForeignKey('establishment_base_types.id')
)
establishment_base_type = relationship('EstablishmentBaseType')
class EstablishmentBaseType(Base):
__tablename__ = 'establishment_base_types'
id = Column(Integer, primary_key=True)

You can use .has on relationships:
#is_qsr.expression
def is_qsr(cls):
return cls.establishment_type.has(
EstablishmentType.establishment_base_type.has(
EstablishmentBaseType.name == "QSR"))
This doesn't produce the most efficient query in the world (it does a EXISTS (SELECT 1 FROM ...)) but a decent optimizer should be able to figure it out.

Related

How do i get multiple foreign keys targetting the same model? (AmbiguousForeignKeysError)

I have two straightforward models like so
class GameModel(db.Model):
__tablename__ = 'games'
id = db.Column(db.Integer, primary_key=True)
home_team = db.Column(db.Integer, db.ForeignKey("teams.team_id"))
away_team = db.Column(db.Integer, db.ForeignKey("teams.team_id"))
class TeamModel(db.Model):
__tablename__ = "teams"
id = db.Column(db.Integer, primary_key=True)
team_id = db.Column(db.Integer, nullable=False, unique=True)
games = db.relationship("GameModel", lazy="joined", backref="game")
when i migrate, i'm getting an error
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship T
eamModel.games - 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.
how do i correctly join these two tables together
You'll need to maintain separate lists of home_games and away_games and then combine them to return the full list of games:
import datetime
import sqlalchemy as sa
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import declarative_base, relationship
connection_uri = (
"mssql+pyodbc://#localhost:49242/myDb?driver=ODBC+Driver+17+for+SQL+Server"
)
engine = sa.create_engine(
connection_uri,
future=True,
echo=False,
)
Base = declarative_base()
class Game(Base):
__tablename__ = "game"
id = sa.Column(sa.Integer, primary_key=True)
when = sa.Column(sa.Date, nullable=False)
home_team_id = sa.Column(sa.Integer, sa.ForeignKey("team.id"))
away_team_id = sa.Column(sa.Integer, sa.ForeignKey("team.id"))
home_team = relationship(
"Team", foreign_keys=[home_team_id], back_populates="home_games"
)
away_team = relationship(
"Team", foreign_keys=[away_team_id], back_populates="away_games"
)
def __repr__(self):
return f"<Game(when='{self.when}')>"
class Team(Base):
__tablename__ = "team"
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String(50))
home_games = relationship(
Game, foreign_keys=[Game.home_team_id], back_populates="home_team"
)
away_games = relationship(
Game, foreign_keys=[Game.away_team_id], back_populates="away_team"
)
#hybrid_property
def games(self):
return self.home_games + self.away_games
def __repr__(self):
return f"<Team(name='{self.name}')>"
# <just for testing>
Base.metadata.drop_all(engine, checkfirst=True)
Base.metadata.create_all(engine)
# </just for testing>
with sa.orm.Session(engine, future=True) as session:
t_a = Team(name="Team_A")
t_b = Team(name="Team_B")
g1 = Game(when=datetime.date(2021, 1, 1), home_team=t_a, away_team=t_b)
g2 = Game(when=datetime.date(2022, 2, 2), home_team=t_b, away_team=t_a)
session.add_all([t_a, t_b, g1, g2])
print(t_a.home_games) # [<Game(when='2021-01-01')>]
print(t_a.away_games) # [<Game(when='2022-02-02')>]
print(t_a.games) # [<Game(when='2021-01-01')>, <Game(when='2022-02-02')>]

Filtering across related models with SQLAlchemy core API (using AIOPG)

I'm trying to do something I'm fairly sure is simple using AIOPG, which can use sqlalchemy's core API. My SQL is not great, so that's where I'm falling down here.
models
class School(Base):
__tablename__ = 'schools'
id = Column(Integer, primary_key=True, nullable=False)
sa_school = School.__table__
class SubjectCategory(Base):
__tablename__ = 'subject_categories'
id = Column(Integer, primary_key=True, nullable=False)
name = Column(String(63))
sa_subject_category = SubjectCategory.__table__
class Subject(Base):
__tablename__ == 'subjects'
id = Column(Integer, primary_key=True, nullable=False)
name = Column(String(63))
category = Column(Integer, ForeignKey('subject_categories.id'), nullable=False)
sa_subject = Subject.__table__
class SchoolSubject(Base):
__tablename__ = 'school_subjects'
id = Column(Integer, primary_key=True, nullable=False)
school = Column(Integer, ForeignKey('schools.id'), nullable=False)
subject = Column(Integer, ForeignKey('subjects.id'), nullable=False)
sa_school_subject = SchoolSubject.__table__
So I'm just trying to get all schools which teach subjects that have a certain subject_category ID.
Currently I have:
from sqlalchemy import select, join
school_c = sa_school.c
school_subj_c = sa_school_subject.c
async def get_schools(subject=None, subj_cat=None)
query = select(
[school_c.id, school_c.name]
).select_from(sa_school.join(sa_school_subject)
if subj_cat:
# Then I need to filter where a school_subj.subject.category == subj_cat
pass
elif subject:
query = query.where(sa_school_subject.c.subject == subj)
cur = await conn.execute(query)
return [dict(b) async for b in cur]
After a bit of faffing, I can simply do:
query = select(
[school_c.id, school_c.name]
).select_from(sa_school.join(sa_school_subject.join(sa_subject)))

Hybrid expression that counts number rows with condition

I have 4 tables:
class Cebola(Base):
__tablename__ = 'cebolas'
id = Column(Integer, primary_key=True, autoincrement=True)
class Checklist(Base):
__tablename__ = 'checklists'
id = Column(Integer, primary_key=True, autoincrement=True)
checklist_type = Column(String)
cebola_id = Column(Integer, ForeignKey('cebolas.id'))
cebola = relationship('Cebola', backref=backref('checklists'))
__mapper_args__ = {'polymorphic_on': checklist_type,
'polymorphic_identity': 'generic'}
class ChecklistA(Checklist):
__tablename__ = 'checklist_a'
id = Column(Integer, ForeignKey('checklists.id', ondelete='CASCADE'), primary_key=True)
notes = Column(Unicode)
__mapper_args__ = {'polymorphic_identity': 'a'}
class ChecklistB(Checklist):
__tablename__ = 'checklist_b'
id = Column(Integer, ForeignKey('checklists.id', ondelete='CASCADE'), primary_key=True)
notes = Column(Unicode)
__mapper_args__ = {'polymorphic_identity': 'b'}
Now I need a way (probably a hybrid property) that will tell me how many checklists I have in a Cebola with notes <> ''.
I have added:
class Cebola(Base):
#hybrid_property
def number_of_comments(self):
return len([c for c in self.checklists if c.notes])
#number_of_comments(cls)
def number_of_comments(cls):
???
I have found a similar problem in SQLAlchemy - Writing a hybrid method for child count, but my example is a bit more complex.
Below should get you started:
class Cebola(Base):
__tablename__ = 'cebolas'
id = Column(Integer, primary_key=True, autoincrement=True)
#hybrid_property
def number_of_comments(self):
# return len([c for c in self.checklists if c.notes])
# #NOTE:1: below will prevent error for those that do not have `notes` column
# #NOTE:2: in any case this may issue a lot of SQL statements to load all subclasses of checklists relationship, so is not really efficient
return len([c for c in self.checklists
if hasattr(c, 'notes') and c.notes])
#number_of_comments.expression
def number_of_comments(cls):
from sqlalchemy import select
ce, cl, cla, clb = [
_.__table__ for _ in (Cebola, Checklist, ChecklistA, ChecklistB)]
s = (
select([func.count(cl.c.id).label("number_of_comments")])
.select_from(cl.outerjoin(cla).outerjoin(clb))
.where(or_(cla.c.notes != None, clb.c.notes != None))
.where(cl.c.cebola_id == cls.id)
.correlate(cls)
.as_scalar()
)
return s
Usage in a query:
q = session.query(Cebola, Cebola.number_of_comments)
for cebola, number_of_comments in q.all():
print(cebola, number_of_comments)

SQLAlchemy Relationships with Single Table Inheritance

I am using SQLAlchemy's Single Table Inheritance for Transaction, StudentTransaction, and CompanyTransaction:
class Transaction(Base):
__tablename__ = 'transaction'
id = Column(Integer, primary_key=True)
# Who paid? This depends on whether the Transaction is a
# CompanyTransaction or a StudentTransaction. We use
# SQLAlchemy's Single Table Inheritance to make this work.
discriminator = Column('origin', String(50))
__mapper_args__ = {'polymorphic_on': discriminator}
# When?
time = Column(DateTime, default=datetime.utcnow)
# Who administered it?
staff_id = Column(Integer, ForeignKey('staff.id'))
staff = relationship(
'Staff',
primaryjoin='and_(Transaction.staff_id==Staff.id)'
)
# How much?
amount = Column(Integer) # Negative for refunds, includes the decimal part
# Type of transaction
type = Column(Enum(
'cash',
'card',
'transfer'
))
class CompanyTransaction(Transaction):
__mapper_args__ = {'polymorphic_identity': 'company'}
company_id = Column(Integer, ForeignKey('company.id'))
company = relationship(
'Company',
primaryjoin='and_(CompanyTransaction.company_id==Company.id)'
)
class StudentTransaction(Transaction):
__mapper_args__ = {'polymorphic_identity': 'student'}
student_id = Column(Integer, ForeignKey('student.id'))
student = relationship(
'Student',
primaryjoin='and_(StudentTransaction.student_id==Student.id)'
)
Then, I have a Student which defines a one-to-many relationship with StudentTransactions:
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
transactions = relationship(
'StudentTransaction',
primaryjoin='and_(Student.id==StudentTransaction.student_id)',
back_populates='student'
)
#hybrid_property
def balance(self):
return sum([transaction.amount for transaction in self.transactions])
The problem is, invoking Student yields: NotImplementedError: <built-in function getitem> for the return line in Student.balance() function.
What am I doing wrong?
Thanks.
a hybrid property is a construct that allows a Python descriptor to be produced which behaves in one way at the instance level, and in another way at the class level. At the class level we wish for it to produce a SQL expression. It's not legal to use plain Python functions like sum() or list comprehensions in order to produce SQL expressions.
In this case, if I were querying from the "student" table and I wished to produce a summation of the "amount" column in the "transaction" table, I'd probably want to use a correlated subquery with a SQL aggregate function. SQL we'd look to see here would resemble:
SELECT * FROM student WHERE (
SELECT SUM(amount) FROM transaction WHERE student_id=student.id) > 500
our hybrid has to take control and produce this expression:
from sqlalchemy import *
from sqlalchemy.orm import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.ext.hybrid import hybrid_property
Base = declarative_base()
class Transaction(Base):
__tablename__ = 'transaction'
id = Column(Integer, primary_key=True)
discriminator = Column('origin', String(50))
__mapper_args__ = {'polymorphic_on': discriminator}
amount = Column(Integer)
class StudentTransaction(Transaction):
__mapper_args__ = {'polymorphic_identity': 'student'}
student_id = Column(Integer, ForeignKey('student.id'))
student = relationship(
'Student',
primaryjoin='and_(StudentTransaction.student_id==Student.id)'
)
class Student(Base):
__tablename__ = 'student'
id = Column(Integer, primary_key=True)
transactions = relationship(
'StudentTransaction',
primaryjoin='and_(Student.id==StudentTransaction.student_id)',
back_populates='student'
)
#hybrid_property
def balance(self):
return sum([transaction.amount for transaction in self.transactions])
#balance.expression
def balance(cls):
return select([
func.sum(StudentTransaction.amount)
]).where(StudentTransaction.student_id==cls.id).as_scalar()
e = create_engine("sqlite://", echo=True)
Base.metadata.create_all(e)
s = Session(e)
s.add_all([
Student(transactions=[StudentTransaction(amount=50), StudentTransaction(amount=180)]),
Student(transactions=[StudentTransaction(amount=600), StudentTransaction(amount=180)]),
Student(transactions=[StudentTransaction(amount=25), StudentTransaction(amount=400)]),
])
print s.query(Student).filter(Student.balance > 400).all()
the output at the end:
SELECT student.id AS student_id
FROM student
WHERE (SELECT sum("transaction".amount) AS sum_1
FROM "transaction"
WHERE "transaction".student_id = student.id) > ?
2014-04-19 19:38:10,866 INFO sqlalchemy.engine.base.Engine (400,)
[<__main__.Student object at 0x101f2e4d0>, <__main__.Student object at 0x101f2e6d0>]

SQLAlchemy: retrieve all episodes from favorite_series of specific user

I have user who can have his favorite series and there are episodes which have series as foreign key and I am trying to retrieve all episodes from favorite series of user.
I am using Flask-SQLAlchemy.
Database:
db = SQLAlchemy(app)
# cross table for user-series
favorite_series = db.Table('favorite_series',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('series_id', db.Integer, db.ForeignKey('series.id'))
)
# user
class User(db.Model):
__tablename__ = 'user'
id = db.Column(db.Integer, primary_key=True)
favorite_series = db.relationship('Series', secondary=favorite_series,
backref=db.backref('users', lazy='dynamic'))
# series
class Series(db.Model):
__tablename__ = 'series'
id = db.Column(db.Integer, primary_key=True)
# episode
class Episode(db.Model):
__tablename__ = 'episode'
id = db.Column(db.Integer, primary_key=True)
series_id = db.Column(db.Integer, db.ForeignKey('series.id'))
series = db.relationship('Series',
backref=db.backref('episodes', lazy='dynamic'))
Friend helped me with SQL
select user_id,series.name,episode.name from (favorite_series left join series on favorite_series.series_id = series.id) left join episode on episode.series_id = series.id where user_id=1;
Altough, I want it in SQLAlchemy API, but can't manage to get it working.
EDIT:
My final working result:
episodes = Episode.query.filter(Episode.series_id.in_(x.id for x in g.user.favorite_series)).filter(Episode.air_time!=None).order_by(Episode.air_time)
First of all you don't seem to be declaring your table names?
Also, the whole point of bothering with orm is so you never have to write sql queries:
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import orm
import sqlalchemy as db
Base = declarative_base()
favorite_series = db.Table('favorite_series', Base.metadata,
db.Column('user_id', db.Integer, db.ForeignKey('User.id')),
db.Column('series_id', db.Integer, db.ForeignKey('Series.id'))
)
class Episode(Base):
__tablename__ = 'Episode'
id = db.Column(db.Integer, primary_key=True)
season = db.Column(db.Integer)
episode_num = db.Column(db.Integer)
series_id = db.Column(db.Integer, db.ForeignKey('Series.id'))
def __init__(self, season, episode_num, series_id):
self.season = season
self.episode_num = episode_num
self.series_id = series_id
def __repr__(self):
return self.series.title + \
' S' + str(self.season) + \
'E' + str(self.episode_num)
class Series(Base):
__tablename__ = 'Series'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String)
episodes = orm.relationship('Episode', backref='series')
def __init__(self, title):
self.title = title
def __repr__(self):
return self.title
class User(Base):
__tablename__ = 'User'
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String)
favorite_series = orm.relationship('Series',
secondary=favorite_series, backref='users')
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
Now you can just access the attributes of your objects and let sql alchemy deal with keeping you DB in sync and issuing queries.
engine = db.create_engine('sqlite:///:memory:')
session = orm.sessionmaker(bind=engine)()
Base.metadata.create_all(engine)
lt = User('Ludovic Tiako')
the_wire = Series('The Wire')
friends = Series('Friends')
session.add_all([lt, the_wire, friends])
session.commit() # need to commit here to generate the id fields
tw_s01e01 = Episode(1,1,the_wire.id)
tw_s01e02 = Episode(1,2,the_wire.id)
f_s01e01 = Episode(1,1,friends.id)
f_s01e02 = Episode(1,2,friends.id)
f_s01e03 = Episode(1,3,friends.id)
session.add_all([tw_s01e01, tw_s01e02,
f_s01e01, f_s01e02, f_s01e03])
session.commit()
the_wire.episodes # > [The Wire S1E1, The Wire S1E2]
friends.episodes # > [Friends S1E1, Friends S1E2, Friends S1E3]
Finally, to answer your question:
lt.favorite_series.append(the_wire)
session.commit()
lt.favorite_series # > [The Wire]
[s.episodes for s in lt.favorite_series] # >> [[The Wire S1E1, The Wire S1E2]]
I don't know about Flask, but from the docs of Flask-SQLAlchemy, it seems it uses declarative, so the ORM. And so, you should have a session. I think it is accessible to you from db.session.
Anyway, if those assumptions are true, this is how you should do it:
query = db.session.query(User.id, Series.name, Episode.name).filter((Episode.series_id == Series.id) & \
(User.id == favorite_series.c.user_id) & (Series.id == favorite_series.c.id) & \
(User.id == 1))
results = query.all();
It might not be the exact query you provided, but should do the same.
UPDATE: I just checked Flask-SQLALchemy code on github, it seems that db is an instance of SQLAlchemy, which has a session attribute, created by self.session = self.create_scoped_session(session_options) which returns a session object. So this should work.
Also, not that by doing that, you won't be using their BaseQuery, although I don't know what that would mean...
Check the documentation to know what to do exactly.

Categories