SQLAlchemy - How to rewrite a query as method of a SQLAlchemy class - python

I have the following two tables defined in SQLAlchemy:
class DataCollection(Base):
__tablename__ = "datacollection"
id = Column(Integer, primary_key=True, index=True)
owner = Column(String, nullable=False)
class Data(Base):
__tablename__ = "data"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, nullable=False)
collection_a_id = Column(Integer, ForeignKey("datacollection.id"))
collection_a = relationship("DataCollection", foreign_keys=[collection_a_id])
collection_b_id = Column(Integer, ForeignKey("datacollection.id"))
collection_b = relationship("DataCollection", foreign_keys=[collection_b_id])
collection_c_id = Column(Integer, ForeignKey("datacollection.id"))
collection_c = relationship("DataCollection", foreign_keys=[collection_c_id])
Given the following data:
data1 = Data(
name="data1",
collection_a=DataCollection(owner="user1"),
collection_b=DataCollection(owner="user1"),
collection_c=DataCollection(owner="user1"),
)
data2 = Data(
name="data2",
collection_a=DataCollection(owner="user1"),
collection_b=DataCollection(owner="user1"),
collection_c=DataCollection(owner="user2"),
)
I want to filter those data objects which are exclusively owned by "user1" using a method owned_by defined on Data:
with Session() as sess:
data = sess.query(Data).filter(Data.owned_by("user1")).all()
assert len(data) == 1
assert data[0].name == "data1"
How can I achieve this?
EDIT:
This is my current solution which does not use a method yet:
from sqlalchemy import (
Column,
String,
Integer,
create_engine,
ForeignKey,
inspect,
)
from sqlalchemy.orm import declarative_base, relationship, sessionmaker, aliased
Base = declarative_base()
class DataCollection(Base):
__tablename__ = "datacollection"
id = Column(Integer, primary_key=True, index=True)
owner = Column(String, nullable=False)
class Data(Base):
__tablename__ = "data"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, unique=True, nullable=False)
collection_a_id = Column(Integer, ForeignKey("datacollection.id"))
collection_a = relationship("DataCollection", foreign_keys=[collection_a_id])
collection_b_id = Column(Integer, ForeignKey("datacollection.id"))
collection_b = relationship("DataCollection", foreign_keys=[collection_b_id])
collection_c_id = Column(Integer, ForeignKey("datacollection.id"))
collection_c = relationship("DataCollection", foreign_keys=[collection_c_id])
engine = create_engine("sqlite:///")
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
def find_data_owned_by(session, user):
DataCollA = aliased(DataCollection)
DataCollB = aliased(DataCollection)
DataCollC = aliased(DataCollection)
query = (
session.query(Data)
.join(DataCollA, Data.collection_a)
.join(DataCollB, Data.collection_b)
.join(DataCollC, Data.collection_c)
.filter(DataCollA.owner == user)
.filter(DataCollB.owner == user)
.filter(DataCollC.owner == user)
)
print(query)
return query.all()
def main():
data1 = Data(
name="data1",
collection_a=DataCollection(owner="user1"),
collection_b=DataCollection(owner="user1"),
collection_c=DataCollection(owner="user1"),
)
data2 = Data(
name="data2",
collection_a=DataCollection(owner="user1"),
collection_b=DataCollection(owner="user1"),
collection_c=DataCollection(owner="user2"),
)
with Session() as sess:
sess.add(data1)
sess.add(data2)
sess.commit()
with Session() as sess:
data = find_data_owned_by(sess, "user1")
assert len(data) == 1
assert data[0].name == "data1"
if __name__ == "__main__":
main()

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)))

How to get parrent of object in session SQLAlchemy

I have two tables:
class Project(DataBase):
__tablename__ = 'projects'
id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
name = Column(String, nullable=False, unique=True)
domain = Column(String, nullable=False)
phrases = relationship("Phrase", backref='proj')
def __init__(self, name, domain):
self.name = name
self.domain = domain
class Phrase(DataBase):
__tablename__ = 'phrases'
query_id = Column(Integer, primary_key=True, autoincrement=True, nullable=False)
query_text = Column(String, nullable=False)
project = Column(Integer, ForeignKey('projects.id'), nullable=False)
enable = Column(Boolean, nullable=False, default=True)
def __init__(self, query_text, project, city):
self.query_text = query_text
self.project = project
And I have a function:
def get_first_query():
session = Session(bind=engine)
q = session.query(Phrase).filter(Phrase.enable == True).first()
session.close()
return q
I want to get an object from table 2 and than get its parrent from first table:
session = Session(bind=engine)
q = get_first_query()
print(q.proj)
It doesn't work and print this error:
sqlalchemy.orm.exc.DetachedInstanceError: Parent instance is not bound to a Session; lazy load operation of
attribute 'proj' cannot proceed
I can do this:
session = Session(bind=engine)
q = get_first_query()
q_project = session.query(Project).filter(Project.id == q.project)
But it's a bad way.
You can assess related object via proj attribute.
session.query(Phrase).filter(
Phrase.enable == True
).first().proj
This way you'll hit database one additional time to get it so you'll need to open session again. To avoid additional queries you can use joined load:
session.query(Phrase).filter(
Phrase.enable == True
).options(
joinedload('proj')
).first().proj

Joining two table without relantioships in sqlalchemy

Here is my problem. I have three tables.
One called Project which has only one Column called id (this must be unique throughout the system).
One called ServiceAwarenessProject which has a one to one relationship with Project.id.
One called CorporateVPNProject which has a one to one relationship with Project.id
I'm using sqlalchemy ORM, so the code looks like the one below:
class Project(SqlAlchemyBase):
__tablename__ = 'project'
id = Column(Integer, primary_key=True, autoincrement=True)
class ServiceAwarenessProject(SqlAlchemyBase):
__tablename__ = 'sa_project'
id = Column(Integer, primary_key=True)
project_id = Column(Integer, ForeignKey(Project.id))
mop_url = Column(String, nullable=False)
expiration_date = Column(Datetime, index=True)
class CorporateVPNProject(SqlAlchemyBase):
__tablename__ = 'wvpn_project'
id = Column(Integer, primary_key=True)
project_id = Column(Integer, ForeignKey(Project.id))
mop_url = Column(String, nullable=False)
I designed my tables like that, so I can guarantee I have unique project_ids in the entire system. My problem is that I don't know how to join those tables together to find a project based on the project_id. To solve this problem for now, I'm querying both tables, using the function called get_project_by_id.
Is there a smarter way to solve this issue?
class ProjectService:
#staticmethod
def create_project_id():
session = DbSessionFactory.create_session()
result = session.query(Project.id).order_by(desc(Project.id)).first()
if result:
result = result[0]
if str(result)[:8] == datetime.datetime.now().strftime('%Y%m%d'):
project_id = str(result)[:8] + '{:03d}'.format(int(str(result)[8:]) + 1)
new_project = Project(id=project_id)
session.add(new_project)
session.commit()
return project_id
project_id = datetime.datetime.now().strftime('%Y%m%d') + '001'
new_project = Project(id=project_id)
session.add(new_project)
session.commit()
return project_id
#staticmethod
def get_project_by_id(project_id):
session = DbSessionFactory.create_session()
result = session.query(ServiceAwarenessProject) \
.filter(ServiceAwarenessProject.project_id == project_id) \
.first()
if result:
return result
result = session.query(CorporateVPNProject) \
.filter(CorporateVPNProject.project_id == project_id) \
.first()
if result:
return result
def create_serviceawareness_project(self):
session = DbSessionFactory.create_session()
project_id = self.create_project_id()
new_project = ServiceAwarenessProject(project_id=project_id, mop_url='http://www.thepacketwizards.com/1')
session.add(new_project)
session.commit()
return new_project
def create_corporatevpn_project(self):
session = DbSessionFactory.create_session()
project_id = self.create_project_id()
new_project = CorporateVPNProject(project_id=project_id, mop_url='http://www.thepacketwizards.com/wvpn')
session.add(new_project)
session.commit()
return new_project
Thank you!
Following #Ilja Everilä suggestion, I designed the table like so, using only joined table inheritance.
class Project(SqlAlchemyBase):
__tablename__ = 'project'
id = Column(Integer, primary_key=True)
created_on = Column(DateTime, default=datetime.datetime.now)
updated_on = Column(DateTime, default=datetime.datetime.now, onupdate=datetime.datetime.now)
project_url = Column(String(60))
mop_url = Column(String(60))
input_url = Column(String(60))
type = Column(String(60))
__mapper_args__ = {
'polymorphic_identity': 'project',
'polymorphic_on': type
}
class ServiceAwarenessProject(Project):
__tablename__ = 'sa_project'
id = Column(Integer, ForeignKey('project.id'), primary_key=True)
expiration_date = Column(DateTime)
__mapper_args__ = {
'polymorphic_identity': 'ServiceAwareness',
}
class CorporateVPNProject(Project):
__tablename__ = 'wvpn_project'
id = Column(Integer, ForeignKey('project.id'), primary_key=True)
client_name = Column(String(60))
__mapper_args__ = {
'polymorphic_identity': 'CorporateVPN',
}
Now, to query the DB I have to use with_polymorphic, so I can get different instances of Tables per row.
class ProjectService:
#staticmethod
def create_project_id():
session = DbSessionFactory.create_session()
result = session.query(Project.id).order_by(desc(Project.id)).first()
print(result)
if result:
result = result[0]
if str(result)[:8] == datetime.datetime.now().strftime('%Y%m%d'):
project_id = str(result)[:8] + '{:03d}'.format(int(str(result)[8:]) + 1)
return project_id
project_id = datetime.datetime.now().strftime('%Y%m%d') + '001'
return project_id
def create_serviceawareness_project(self):
session = DbSessionFactory.create_session()
project_id = self.create_project_id()
new_project = ServiceAwarenessProject(id=project_id,
project_url='http://project',
expiration_date=datetime.datetime.now() + datetime.timedelta(days=365),
mop_url='http://mop',
input_url='http://url',
type='ServiceAwareness')
session.add(new_project)
session.commit()
session.add(new_project)
return new_project
def create_corporatevpn_project(self):
session = DbSessionFactory.create_session()
project_id = self.create_project_id()
new_project = CorporateVPNProject(id=project_id,
project_url='http://project',
client_name='TIM',
mop_url='http://mop',
input_url='http://url',
type='CorporateVPN')
session.add(new_project)
session.commit()
session.add(new_project)
return new_project
#staticmethod
def get_project_by_id(project_id):
session = DbSessionFactory.create_session()
query = session.query(with_polymorphic(Project, [ServiceAwarenessProject, CorporateVPNProject])).filter(or_(
ServiceAwarenessProject.id == project_id,
CorporateVPNProject.id == project_id
)).first()
return query

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)

Categories