SQLAlchemy Relationships with Single Table Inheritance - python

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

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

How to specify the join condition on a many-to-many relationship using SQLAlchemy

Here is my model:
user_map = Table(
"user_map",
Column('user_id', Integer, ForeignKey('user.id'), primary_key=True),
Column('map_id', Integer, ForeignKey('map.id'), primary_key=True),
PrimaryKeyConstraint('user_id', 'map_id', name='pk_user_map')
)
class Map(Base):
id = Column(Integer, primary_key=True)
name = Column(String)
owner_id = Column(Integer, ForeignKey('user.id'))
shared_maps = relationship(
'User',
secondary=user_map,
backref=backref('maps', lazy='dynamic')
)
class User(Base):
id = Column(Integer, primary_key=True)
name = Column(String)
email = Column(String, unique=True)
shared_maps = Map.query.filter(Map.shared_maps.any()).all()
I want to query the user_map table, using the join condition "Map.id == user_map.map_id", but SQLAlchemy is trying to join using "Map.id == user_map.map_id and Map.owner_id == user_map.user_id". How can I specify my join condition?
I tried to use primaryjoin attribute in the relationship and to specify the condition inside the .join() but without success. Thanks in advance!
Based on your code, I've rebuilt the setup; I guess your relationships were mixed up. Furthermore, I've hardly ever seen primary keys (or PrimaryKeyConstraints) in sqlalchemy's many-to-many association tables. It may make sense from a non-orm perspective, but as far as I know, it is unusual or even not required at all.
import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
Base = declarative_base()
UsersXMaps = sa.Table(
'users_x_maps',
Base.metadata,
sa.Column('user', sa.Integer, sa.ForeignKey('users.id')),
sa.Column('map', sa.Integer, sa.ForeignKey('maps.id'))
)
class User(Base):
__tablename__ = 'users'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
mail = sa.Column(sa.String, unique=True)
own_maps = relationship('Map', back_populates='owner')
maps = relationship(
'Map',
secondary=UsersXMaps,
back_populates='users'
)
def __str__(self):
return '{} ({}) with {} maps'.format(
self.name, self.mail, len(self.own_maps))
class Map(Base):
__tablename__ = 'maps'
id = sa.Column(sa.Integer, primary_key=True)
name = sa.Column(sa.String)
owner_id = sa.Column(sa.Integer, sa.ForeignKey('users.id'))
owner = relationship('User', back_populates='own_maps')
users = relationship(
'User',
secondary=UsersXMaps,
back_populates='maps'
)
def __str__(self):
return '{} (by {})'.format(self.name, self.owner.name)
So far for the setup; I've extended it a bit for proper output when printing strings. Additionally, your Map.shared_maps relationship actually refers to Users, not Maps, so I also renamed that one.
When binding your association table to the two classes, you can refer to it from both sides (even though back_populates seems to overwrite/replace the original definition) - this simplifies joins from either side.
Executing the following works as expected:
if __name__ == '__main__':
engine = sa.create_engine('sqlite:///usermaps.db')
sfactory = sessionmaker(engine)
session = sfactory()
Base.metadata.create_all(bind=engine)
bilbo = User(id=1, name='Bilbo', mail='bilbo#shire.nz')
frodo = User(id=2, name='Frodo', mail='frodo#shire.nz')
mordor = Map(id=1, name='Mordor', owner=frodo, users=[bilbo, frodo])
gondor = Map(id=2, name='Gondor', owner=bilbo, users=[bilbo, frodo])
rohan = Map(id=3, name='Rohan', owner=bilbo, users=[bilbo, frodo])
session.add_all([frodo, bilbo, mordor, gondor, rohan])
session.commit()
print('Maps by owner:')
for owner in [bilbo, frodo]:
print(owner)
for item in session.query(Map).filter(Map.owner == owner).all():
print(' - ' + str(item))
print('Maps by users:')
for item in session.query(Map).filter(Map.users.any()).all():
print(' - ' + str(item))
The output is:
Maps by owner:
Bilbo (bilbo#shire.nz) with 2 maps
- Gondor (by Bilbo)
- Rohan (by Bilbo)
Frodo (frodo#shire.nz) with 1 maps
- Mordor (by Frodo)
Maps by users:
- Mordor (by Frodo)
- Gondor (by Bilbo)
- Rohan (by Bilbo)

nested relationship in hybrid_property in sqlalchemy

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.

Self referential relationship including a relationship attribute

Situation
I have the Self-Referential Many-to-Many Relationship (almost identical to the sqlalchemy manual entry of the same heading). This relationship is governed by the table entity_weights. This code works!
Question
How do I include an attribute in class Entity representing the table column entity_weights.weight. Lets say the attribute would be called Entity.child_weights. It is important that the rank of Entity.child_entities and Entity.child_weights are identical.
entity_weights = Table('entity_weights', Base.metadata,
Column('id',Integer, primary_key=True),
Column('parent_entity_id',Integer, ForeignKey('entity.id')),
Column('entity_id',Integer, ForeignKey('entity.id')),
Column('weight',Float))
class Entity(Base):
__tablename__ = 'entity'
id = Column(Integer, primary_key=True)
name = Column(String)
domicile_id = Column(Integer, ForeignKey('domicile.id'))
entity_type = Column('type',Enum('asset','institution','model'))
source_table_id = Column(Integer)
child_entities = relationship('Entity',
secondary=entity_weights,
primaryjoin=id==entity_weights.c.parent_entity_id,
secondaryjoin=id==entity_weights.c.entity_id,
backref='parent_entity_id'
)
The cleanest solution I've found for this scenario is to break up the child_entities relationship by adding entity_weights as a one-to-many relationship on Entity and use an association proxy to proxy the weight value as well as the remote side of the many-to-many relationship:
class EntityWeight(Base):
__tablename__ = "entity_weights"
id = Column(Integer, primary_key=True)
parent_entity_id = Column(Integer, ForeignKey('entity.id'))
entity_id = Column(Integer, ForeignKey('entity.id'))
weight = Column(Float)
entity = relationship("Entity", primaryjoin=lambda: EntityWeight.entity_id == Entity.id)
class Entity(Base):
...
_child_weights = relationship(EntityWeight, primaryjoin=id == EntityWeight.parent_entity_id)
child_weights = association_proxy("_child_weights", "weight")
child_entities = association_proxy("_child_weights", "entity")

How to build many-to-many relations using SQLAlchemy: a good example

I have read the SQLAlchemy documentation and tutorial about building many-to-many relation but I could not figure out how to do it properly when the association table contains more than the 2 foreign keys.
I have a table of items and every item has many details. Details can be the same on many items, so there is a many-to-many relation between items and details
I have the following:
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
My association table is (It's defined before the other 2 in the code):
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
In the documentation, it's said that I need to use the "association object". I could not figure out how to use it properly, since it's mixed declarative with mapper forms and the examples seem not to be complete. I added the line:
details = relation(ItemDetail)
as a member of Item class and the line:
itemDetail = relation('Detail')
as a member of the association table, as described in the documentation.
when I do item = session.query(Item).first(), the item.details is not a list of Detail objects, but a list of ItemDetail objects.
How can I get details properly in Item objects, i.e., item.details should be a list of Detail objects?
From the comments I see you've found the answer. But the SQLAlchemy documentation is quite overwhelming for a 'new user' and I was struggling with the same question. So for future reference:
ItemDetail = Table('ItemDetail',
Column('id', Integer, primary_key=True),
Column('itemId', Integer, ForeignKey('Item.id')),
Column('detailId', Integer, ForeignKey('Detail.id')),
Column('endDate', Date))
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail, backref='Detail')
Like Miguel, I'm also using a Declarative approach for my junction table. However, I kept running into errors like
sqlalchemy.exc.ArgumentError: secondary argument <class 'main.ProjectUser'> passed to to relationship() User.projects must be a Table object or other FROM clause; can't send a mapped class directly as rows in 'secondary' are persisted independently of a class that is mapped to that same table.
With some fiddling, I was able to come up with the following. (Note my classes are different than OP's but the concept is the same.)
Example
Here's a full working example
from sqlalchemy import create_engine, Column, Integer, String, ForeignKey
from sqlalchemy.orm import declarative_base, relationship, Session
# Make the engine
engine = create_engine("sqlite+pysqlite:///:memory:", future=True, echo=False)
# Make the DeclarativeMeta
Base = declarative_base()
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True)
name = Column(String)
projects = relationship('Project', secondary='project_users', back_populates='users')
class Project(Base):
__tablename__ = "projects"
id = Column(Integer, primary_key=True)
name = Column(String)
users = relationship('User', secondary='project_users', back_populates='projects')
class ProjectUser(Base):
__tablename__ = "project_users"
id = Column(Integer, primary_key=True)
notes = Column(String, nullable=True)
user_id = Column(Integer, ForeignKey('users.id'))
project_id = Column(Integer, ForeignKey('projects.id'))
# Create the tables in the database
Base.metadata.create_all(engine)
# Test it
with Session(bind=engine) as session:
# add users
usr1 = User(name="bob")
session.add(usr1)
usr2 = User(name="alice")
session.add(usr2)
session.commit()
# add projects
prj1 = Project(name="Project 1")
session.add(prj1)
prj2 = Project(name="Project 2")
session.add(prj2)
session.commit()
# map users to projects
prj1.users = [usr1, usr2]
prj2.users = [usr2]
session.commit()
with Session(bind=engine) as session:
print(session.query(User).where(User.id == 1).one().projects)
print(session.query(Project).where(Project.id == 1).one().users)
Notes
reference the table name in the secondary argument like secondary='project_users' as opposed to secondary=ProjectUser
use back_populates instead of backref
I made a detailed writeup about this here.
Previous Answer worked for me, but I used a Class base approach for the table ItemDetail. This is the Sample code:
class ItemDetail(Base):
__tablename__ = 'ItemDetail'
id = Column(Integer, primary_key=True, index=True)
itemId = Column(Integer, ForeignKey('Item.id'))
detailId = Column(Integer, ForeignKey('Detail.id'))
endDate = Column(Date)
class Item(Base):
__tablename__ = 'Item'
id = Column(Integer, primary_key=True)
name = Column(String(255))
description = Column(Text)
details = relationship('Detail', secondary=ItemDetail.__table__, backref='Item')
class Detail(Base):
__tablename__ = 'Detail'
id = Column(Integer, primary_key=True)
name = Column(String)
value = Column(String)
items = relationship('Item', secondary=ItemDetail.__table__, backref='Detail')

Categories