SQLAlchemy ORM: proxy attribute pointing to the first element of a relation? - python

Starting from the many-to-many relationship example from the SQLAlchemy documentation, I want to add an attribute first_child that will return the first child of children defined by the relationship. The first_child attribute needs to be useable in an association_proxy attribute definition such as first_child_id below.
from sqlalchemy import Table, Column, Integer, ForeignKey
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
association_table = Table('association', Base.metadata,
Column('left_id', Integer, ForeignKey('left.id')),
Column('right_id', Integer, ForeignKey('right.id'))
)
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship("Child", secondary=association_table)
first_child = ???
first_child_id = association_proxy('first_child', 'id')
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
I'm thinking I need a to declare first_child as either a hybrid_property or a column_property, but I don't know how to return the first element.
In addition to first_child, I also need last_child and an associated last_child_id attribute.
I'm using SQLAlchemy with a MySQL database.

If what you need is to have minimum start_time and maximum end_time, then I would use column_property just for these columns:
association_table = Table(
'association', Base.metadata,
Column('left_id', Integer, ForeignKey('left.id')),
Column('right_id', Integer, ForeignKey('right.id'))
)
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
start_time = Column(DateTime)
end_time = Column(DateTime)
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship(
"Child",
secondary=association_table,
backref="parents",
)
min_start_time = column_property(
select([Child.start_time.label("min_start_time")])
.where(Child.id == association_table.c.right_id)
.where(id == association_table.c.left_id)
.order_by(Child.start_time.asc())
.limit(1)
)
max_end_time = column_property(
select([Child.end_time.label("max_end_time")])
.where(Child.id == association_table.c.right_id)
.where(id == association_table.c.left_id)
.order_by(Child.end_time.desc())
.limit(1)
.as_scalar()
)
But if you need more than one such special column from the relationship, probably it would be more efficient to use hybrid_property.

The problem with association_proxy is that you cannot use it on hybrid_properties (or at least not in a direct manner).
A simple solution might be to use a property decorator, which would be evaluated on an already-loaded instance:
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship("Child", secondary=association_table)
#property
def first_child(self):
return children[0]
However, you will not be able to use it on queries and it is a "read only" property. More information on SQLAlchemy plain-descriptor.

Related

How do I *dynamically* set the schema in SQLAlchemy for MSSQL?

On this question I learned how to set the schema on an ORM definition:
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class Notification(Base):
__tablename__ = "dog"
__table_args__ = {"schema": "animal"}
id = Column(Integer, primary_key=True)
name = Column(String)
But I now need to make the schema configurable. I have tried passing the table_args parameter at object creation, but it's still trying the schema I put on the class definition.
The better solution I have found so far is to create a function that returns the class:
function get_notification_class(schema: str):
class Notification(Base):
__tablename__ = "dog"
__table_args__ = {"schema": schema}
id = Column(Integer, primary_key=True)
name = Column(String)
return Notification
And then, to use it
Notification = get_notification_class('animal')
obj = Notification('1', 'doggy')

SQLAlchemy: AmbiguousForeignKeysError double reference to parent

I have the following simplified code for Groups that can have subgroups with roles:
from sqlalchemy import (
CheckConstraint,
Column,
ForeignKey,
PrimaryKeyConstraint,
String,
create_engine,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker, relationship
Base = declarative_base()
class Group(Base):
__tablename__ = 'groups'
name = Column(String, primary_key=True)
group_roles = relationship('GroupRole', back_populates='group')
class GroupRole(Base):
__tablename__ = 'group_roles'
name = Column(String, ForeignKey(Group.name))
group_name = Column(String, ForeignKey(Group.name))
role = Column(String, nullable=False)
__table_args__ = (
CheckConstraint('name != group_name'),
PrimaryKeyConstraint('name', 'group_name'),
)
group = relationship(Group,
foreign_keys=[name],
back_populates='group_roles')
engine = create_engine('sqlite:///:memory:')
Base.metadata.create_all(engine)
session = sessionmaker(bind=engine)()
session.add(Group(name='test'))
When I run this, I get sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Group.group_roles - there are multiple foreign key path
s 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.
I have also tried adding the following below the GroupRole table in lieu of the corresponding line already defined in Group:
Group.group_roles = relationship('GroupRole', foreign_keys=[Column(String, ForeignKey(GroupRole.name))], back_populates='group')
But I get the same error, and it doesn't look like others have had to do this. What am I doing wrong? How can I get my GroupRole table to reference my Group table? I'm using SQLAlchemy 1.3 and python 3.7.
You can make the code work with only backref on the GroupRole.group relationship, such as:
class Group(Base):
__tablename__ = 'groups'
name = Column(String, primary_key=True)
class GroupRole(Base):
__tablename__ = 'group_roles'
name = Column(String, ForeignKey(Group.name))
group_name = Column(String, ForeignKey(Group.name))
role = Column(String, nullable=False)
__table_args__ = (
CheckConstraint('name != group_name'),
PrimaryKeyConstraint('name', 'group_name'),
)
group = relationship(Group,
foreign_keys=[name],
backref='group_roles')
Not sure this is what is intended though, but working:
g = Group(name='test', group_roles=[
GroupRole(group_name='testrole', name='test', role='testrole'),
GroupRole(group_name='testrole2', name='test', role='testrole2')
])
session.add(g)

Self-Referential Many-to-Many Relationship with Association Proxy in SQLAlchemy

I'm trying to create a Self-Referential Many-to-Many Relationship. The example outlined in the SQLAlchemy documentation works great. Here are the models I created:
from sqlalchemy import Integer, ForeignKey, String, Column, Table
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
Table('NodeToNode', Base.metadata,
Column('leftNodeId', Integer, ForeignKey('Node.id'), primary_key=True),
Column('rightNodeId', Integer, ForeignKey('Node.id'), primary_key=True)
)
class Node(Base):
__tablename__ = 'Node'
id = Column(Integer, primary_key=True)
label = Column(String)
rightNodes = relationship('Node',
secondary='NodeToNode',
primaryjoin='Node.id==NodeToNode.c.leftNodeId',
secondaryjoin='Node.id==NodeToNode.c.rightNodeId',
backref='leftNodes'
)
And the script for adding data in:
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
engine = create_engine('sqlite:///practice.sqlite3')
session = Session(bind=engine)
nodes = [
Node(label='A'),
Node(label='B'),
Node(label='C'),
Node(label='D'),
Node(label='E'),
]
nodes[0].rightNodes = [nodes[1], nodes[3], nodes[2]]
nodes[0].leftNodes = [nodes[4]]
session.add_all(nodes)
session.commit()
I want to add a column to the association table so I'd assume I need to convert the association table to its own class:
class NodeToNode(Base):
__tablename__ = 'NodeToNode'
leftNodeId = Column(Integer, ForeignKey('Node.id', onupdate='CASCADE'), primary_key=True)
rightNodeId = Column(Integer, ForeignKey('Node.id', onupdate='CASCADE'), primary_key=True)
sortOrder = Column(Integer, nullable=False)
This, however, results in the following error:
sqlalchemy.exc.InvalidRequestError: Class <class 'models.node.NodeToNode'> does not have a mapped column named 'c'
Any idea what I'm doing wrong here?

Many-to-many relationship traversal with SQLAlchemy

I'm quite new to SQLAlchemy (and I do not have much experience with databases in general). I'm trying to traversere two many-to-many relationships. Given a parent, how can I get all unique grandchildren?
parent_child_table = Table('parent_child', Base.metadata,
Column('parent_id', Integer, ForeignKey('parent.id')),
Column('child_id', Integer, ForeignKey('child.id'))
)
class Parent(Base):
__tablename__ = 'parent'
id = Column(Integer, primary_key=True)
children = relationship("Child",
secondary=parent_child_table,
backref="parents")
child_grandchild_table = Table('child_grandchild', Base.metadata,
Column('child_id', Integer, ForeignKey('child.id')),
Column('grandchild_id', Integer, ForeignKey('grandchild.id'))
)
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
grandchildren = relationship("Grandchild",
secondary=child_grandchild_table,
backref="children")
class Grandchild(Base):
__tablename__ = 'grandchild'
id = Column(Integer, primary_key=True)
Thanks! This problem is giving me a headache...
The most straight-forard way:
# my_parent = ... (instance of Parent)
q = (session.query(Grandchild)
.join(Child, Grandchild.children)
.join(Parent, Child.parents)
.filter(Parent.id == my_parent.id)
)
sqlalchemy will return only unique Grandchild instances (although the SQL query does not filter duplicates out).

can't insert into one-to-many relationship in sqlalchemy

I have a one to many relationship in sqlalchemy but I don't get the inserts to work properly. I have tried to make a minimal example here:
from sqlalchemy import *
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker
db = create_engine('sqlite://')
db.echo = True
metadata = MetaData(db)
Base = declarative_base()
Session = sessionmaker(bind=db)
session = Session()
class Child(Base):
__table__ = Table('child', Base.metadata,
Column('id', Integer, primary_key=True),
Column('parent_id', Integer),
Column('name', String(50))
)
class Parent(Base):
__table__ = Table('parent', Base.metadata,
Column('id', Integer, primary_key=True),
Column('name', String(50))
)
children = relationship(Child, primaryjoin="Parent.id == Child.parent_id",
foreign_keys=[__table__.c.id])
Base.metadata.create_all(db)
c = Child(id=1, name="C")
p = Parent(id=1, name="P", children=[c])
session.add(p)
session.commit()
Running this gives AttributeError: 'list' object has no attribute '_sa_instance_state' from session.add(p).
I tried changing the classes to this:
class Child(Base):
__tablename__ = 'child'
id = Column('id', Integer, primary_key=True)
parent_id = Column('parent_id', Integer, ForeignKey('parent.id'))
name = Column('name', String(50))
class Parent(Base):
__tablename__ = 'parent'
id = Column('id', Integer, primary_key=True)
name = Column('name', String(50))
children = relationship(Child, backref="parent")
and then it works. I specify that the parent_id is a foreign key there and use the backref syntax. However in my production code the Parent table is a temporary table so I can't directly reference it using a ForeignKey. So whats wrong with the first code block and how can it be fixed ?
From the SQLAlchemy relationship API in the docs:
That is, if the primaryjoin condition of this relationship() is a.id == b.a_id, and the values in b.a_id are required to be present in a.id, then the “foreign key” column of this relationship() is b.a_id.
In your example, child.parent_id is required to be present in parent.id. So your "foreign key" column is child.parent_id.
Therefore, changing:
foreign_keys=[__table__.c.id]
to this:
foreign_keys=[Child.__table__.c.parent_id]
should solve your problem.

Categories

Resources