In a database that I have no control over I have what I believe is a Generic Association using generic foreign keys. In short: A CommunicationInterface belongs either to a Router or a Server, never both. There's no discriminator because ids are unique over the entire database.
Code shortened for brevity:
class Server(...):
__tablename__ = 'server'
id = Column('_oid', Integer, primary_key=True)
interfaces = relationship(
'CommunicationInterface', backref='server',
primaryjoin='Server.id == foreign(CommunicationInterface.parent_id)'
)
class Router(...):
__tablename__ = 'router'
id = Column('_oid', Integer, primary_key=True)
interfaces = relationship(
'CommunicationInterface', backref='router',
primaryjoin='Router.id == foreign(CommunicationInterface.parent_id)'
)
class CommunicationInterface(...):
__tablename__ = 'communicationinterface'
id = Column('_oid', Integer, primary_key=True)
parent_id = Column('_parent_oid', Integer)
overlaps = 'router,server'
router = relationship(
'Router', back_populates='interfaces', overlaps=overlaps,
primaryjoin='Router.id == foreign(CommunicationInterface.parent_id)'
)
server = relationship(
'Server', back_populates='interfaces', overlaps=overlaps,
primaryjoin='Server.id == foreign(CommunicationInterface.parent_id)'
)
Even with the overlap parameter this still gives me warnings like the following:
SAWarning: relationship 'Router.interfaces' will copy column router._oid to column communicationinterface._parent_oid, which conflicts with relationship(s): 'CommunicationInterface.server' (copies elb_server._oid to elb_communicationinterface._parent_oid). If this is not the intention, consider if these relationships should be linked with back_populates, or if viewonly=True should be applied to one or more if they are read-only. For the less common case that foreign key constraints are partially overlapping, the orm.foreign() annotation can be used to isolate the columns that should be written towards. The 'overlaps' parameter may be used to remove this warning.
Is there a better way to declare these relations? And if not, am I using the overlaps parameter wrong?
The issue that you faced has a few reasons:
Mixing of backref and back_populates
backref also creates a relationship on the other side
back_populates expects a relationship on the other side
overlaps incorrect missing on the relationship on the other side
This should solve your problem:
class Server(Base):
__tablename__ = 'server'
id = Column('_oid', Integer, primary_key=True)
class Router(Base):
__tablename__ = 'router'
id = Column('_oid', Integer, primary_key=True)
class CommunicationInterface(Base):
__tablename__ = 'communicationinterface'
id = Column('_oid', Integer, primary_key=True)
parent_id = Column('_parent_oid', Integer)
router = relationship(
'Router', backref=backref('interface', lazy='subquery', overlaps='server, interface'),
primaryjoin='Router.id == foreign(CommunicationInterface.parent_id)'
)
server = relationship(
'Server', backref=backref('interface', lazy='subquery', overlaps='router'), overlaps='router',
primaryjoin='Server.id == foreign(CommunicationInterface.parent_id)'
)
Or if you want to do it with back_populates:
class Server(Base):
__tablename__ = 'server'
id = Column('_oid', Integer, primary_key=True)
interface = relationship(
'CommunicationInterface', back_populates='server', overlaps='router',
primaryjoin='Server.id == foreign(CommunicationInterface.parent_id)'
)
class Router(Base):
__tablename__ = 'router'
id = Column('_oid', Integer, primary_key=True)
interface = relationship(
'CommunicationInterface', back_populates='router', overlaps='server, interface',
primaryjoin='Router.id == foreign(CommunicationInterface.parent_id)'
)
class CommunicationInterface(Base):
__tablename__ = 'communicationinterface'
id = Column('_oid', Integer, primary_key=True)
parent_id = Column('_parent_oid', Integer)
router = relationship(
'Router', back_populates='interface',
primaryjoin='Router.id == foreign(CommunicationInterface.parent_id)'
)
server = relationship(
'Server', back_populates='interface', overlaps='router',
primaryjoin='Server.id == foreign(CommunicationInterface.parent_id)'
)
Related
Background
The documentation gives the following example of a parent-child-association being added by appending that association to p.children. The child is then accessed via p.children and I assume the other way is possible as well, i.e accessing the parent via c.parents.
class Association(Base):
__tablename__ = 'association'
left_id = Column(ForeignKey('left.id'), primary_key=True)
right_id = Column(ForeignKey('right.id'), primary_key=True)
extra_data = Column(String(50))
child = relationship("Child", back_populates="parents")
parent = relationship("Parent", back_populates="children")
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship("Association", back_populates="parent")
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
parents = relationship("Association", back_populates="child")
# create parent, append a child via association
p = Parent()
a = Association(extra_data="some data")
a.child = Child()
p.children.append(a)
# iterate through child objects via association, including association
# attributes
for assoc in p.children:
print(assoc.extra_data)
print(assoc.child)
Problem
I have three classes that are almost identical to those in the example:
class TopicSubcription(Base):
__tablename__ = 'topic_subscription'
topic_id = Column(ForeignKey('topic.id'), primary_key=True)
user_id = Column(ForeignKey('user.id'), primary_key=True)
subscription_date = Column('subscription_date', DateTime, nullable=True)
topic = relationship("Topic", back_populates="subscribed_users")
user = relationship("User", back_populates="followed_topics")
class Topic(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
subscribed_users = relationship("Association", back_populates="topic")
class User(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
followed_topics = relationship("Association", back_populates="user")
but when I run the following I get a NotNullViolation error stating that user_id violates not-null constaint.
u: User = session.query(User).filter(User.id == 'example_id1').one()
t: Topic = session.query(Topic).filter(Topic.id == 'example_id2').one()
sub = TopicSubscription(func.now())
sub.topic = t
u.followed_topics.append(sub)
session.add(u)
session.commit()
Question
Am I misinterpreting the documentation or is it wrong?
I had something quite similar just now and was going crazy trying to work it out. Abstracting to the parent/child example from the docs, what worked in my case was:
p = Parent()
a = Association(parent=p, extra_data="some data")
a.child = Child()
p.children.append(a)
I have two models:
class Profile(Base):
__tablename__ = 'profiles'
id = Column(Integer, primary_key=True)
...
stagesP_list = relationship(
'StageP',
back_populates='profiles_list',
secondary=stageP_profile
)
class Project(Base):
__tablename__ = 'projects'
id = Column(Integer, primary_key=True)
...
stagesP_list = relationship(
'StageP',
back_populates='projects_list',
secondary=stageP_project
)
I need to select Profiles for which at least one value of the Profile.stagesP_list is contained in the project.stagesP_list.
Please help to compose the query or indicate the direction in which to search.
If you have project instance loaded, you can compose the following query:
project = ...
stageP_ids = [obj.id for obj in project.stagesP_list]
query = session.query(Profile).filter(
Profile.stagesP_list.any(StageP.id.in_(stageP_ids))
)
You can also perform joins on the database directly from having only project_id:
query = (
session.query(Profile)
.join(StageP, Profile.stagesP_list)
.join(Project, StageP.projects_list)
.where(Project.id == project_id)
.distinct()
)
This is the way that I usually use for m2m relationship implementation.
(Brought from docs.sqlalchemy.org)
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,
backref="parents")
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
Is there any way for using additional columns at the association_table table?
So it should be like
association_table = Table('association', Base.metadata,
Column('left_id', Integer, ForeignKey('left.id')),
Column('right_id', Integer, ForeignKey('right.id')),
Column('is_valid', Boolean, default=True) # Add the validation column
)
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship("Child",
secondary=association_table,
backref="parents")
# How can I do implement this??
valid_children = relationship("Child",
secondary="and_(association_table.left_id == Parent.id, association_table.right_id == Child.id)"
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
I want to do query depends on is_valid column. How can I modify "secondary" attr in Parent table? Or should I fix the other part?
In this question, time_create column has the same value for all children. But in this case, I need a flag that makes able to retrieve whether this connection is still alive or not.
For example, if you implement a one-on-one chatting, there will be a chatting room consist of two-person, right?
And the table should be like as below:
association_table = Table('association', Base.metadata,
Column('left_id', Integer, ForeignKey('left.id')),
Column('right_id', Integer, ForeignKey('right.id')),
Column('is_left', Boolean, default=False) # Whether the user left or not
)
class Match(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
user = relationship("User",
secondary=association_table,
backref="matches")
# How can I do implement this??
exist_user = relationship("User",
secondary="and_(association_table.left_id == Parent.id, association_table.right_id == Child.id)"
class User(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
nickname = Column(String, unique=True)
How can I do for this?
I know i can define a table using Table:
user = Table('user', metadata,
Column('user_id', Integer, primary_key = True),
)
and using Base:
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
user_id= Column(Integer, primary_key = True)
but what's the different???
Base = declarative_base()
class User(Base):
__tablename__ = 'user'
id = Column('id', Integer, primary_key=True)
name = Column('name', Unicode(64))
is just syntactic sugar for
metadata = MetaData()
user = Table('user', metadata,
Column('id', Integer, primary_key=True),
Column('name', Unicode(64)),
)
class User(object):
pass
mapper(User, user) # this will modify the User class!
I have a star-schema architectured database that I want to represent in SQLAlchemy. Now I have the problem on how this can be done in the best possible way. Right now I have a lot of properties with custom join conditions, because the data is stored in different tables.
It would be nice if it would be possible to re-use the dimensions for different fact tablesw but I haven't figured out how that can be done nicely.
A typical fact table in a star schema contains foreign key references to all dimension tables, so usually there wouldn't be any need for custom join conditions - they are determined automatically from foreign key references.
For example a star schema with two fact tables would look like:
Base = declarative_meta()
class Store(Base):
__tablename__ = 'store'
id = Column('id', Integer, primary_key=True)
name = Column('name', String(50), nullable=False)
class Product(Base):
__tablename__ = 'product'
id = Column('id', Integer, primary_key=True)
name = Column('name', String(50), nullable=False)
class FactOne(Base):
__tablename__ = 'sales_fact_one'
store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
product_id = Column('product_id', Integer, ForeignKey('product.id'), primary_key=True)
units_sold = Column('units_sold', Integer, nullable=False)
store = relation(Store)
product = relation(Product)
class FactTwo(Base):
__tablename__ = 'sales_fact_two'
store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
product_id = Column('product_id', Integer, ForeignKey('product.id'), primary_key=True)
units_sold = Column('units_sold', Integer, nullable=False)
store = relation(Store)
product = relation(Product)
But suppose you want to reduce the boilerplate in any case. I'd create generators local to the dimension classes which configure themselves on a fact table:
class Store(Base):
__tablename__ = 'store'
id = Column('id', Integer, primary_key=True)
name = Column('name', String(50), nullable=False)
#classmethod
def add_dimension(cls, target):
target.store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
target.store = relation(cls)
in which case usage would be like:
class FactOne(Base):
...
Store.add_dimension(FactOne)
But, there's a problem with that. Assuming the dimension columns you're adding are primary key columns, the mapper configuration is going to fail since a class needs to have its primary keys set up before the mapping is set up. So assuming we're using declarative (which you'll see below has a nice effect), to make this approach work we'd have to use the instrument_declarative() function instead of the standard metaclass:
meta = MetaData()
registry = {}
def register_cls(*cls):
for c in cls:
instrument_declarative(c, registry, meta)
So then we'd do something along the lines of:
class Store(object):
# ...
class FactOne(object):
__tablename__ = 'sales_fact_one'
Store.add_dimension(FactOne)
register_cls(Store, FactOne)
If you actually have a good reason for custom join conditions, as long as there's some pattern to how those conditions are created, you can generate that with your add_dimension():
class Store(object):
...
#classmethod
def add_dimension(cls, target):
target.store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
target.store = relation(cls, primaryjoin=target.store_id==cls.id)
But the final cool thing if you're on 2.6, is to turn add_dimension into a class decorator. Here's an example with everything cleaned up:
from sqlalchemy import *
from sqlalchemy.ext.declarative import instrument_declarative
from sqlalchemy.orm import *
class BaseMeta(type):
classes = set()
def __init__(cls, classname, bases, dict_):
klass = type.__init__(cls, classname, bases, dict_)
if 'metadata' not in dict_:
BaseMeta.classes.add(cls)
return klass
class Base(object):
__metaclass__ = BaseMeta
metadata = MetaData()
def __init__(self, **kw):
for k in kw:
setattr(self, k, kw[k])
#classmethod
def configure(cls, *klasses):
registry = {}
for c in BaseMeta.classes:
instrument_declarative(c, registry, cls.metadata)
class Store(Base):
__tablename__ = 'store'
id = Column('id', Integer, primary_key=True)
name = Column('name', String(50), nullable=False)
#classmethod
def dimension(cls, target):
target.store_id = Column('store_id', Integer, ForeignKey('store.id'), primary_key=True)
target.store = relation(cls)
return target
class Product(Base):
__tablename__ = 'product'
id = Column('id', Integer, primary_key=True)
name = Column('name', String(50), nullable=False)
#classmethod
def dimension(cls, target):
target.product_id = Column('product_id', Integer, ForeignKey('product.id'), primary_key=True)
target.product = relation(cls)
return target
#Store.dimension
#Product.dimension
class FactOne(Base):
__tablename__ = 'sales_fact_one'
units_sold = Column('units_sold', Integer, nullable=False)
#Store.dimension
#Product.dimension
class FactTwo(Base):
__tablename__ = 'sales_fact_two'
units_sold = Column('units_sold', Integer, nullable=False)
Base.configure()
if __name__ == '__main__':
engine = create_engine('sqlite://', echo=True)
Base.metadata.create_all(engine)
sess = sessionmaker(engine)()
sess.add(FactOne(store=Store(name='s1'), product=Product(name='p1'), units_sold=27))
sess.commit()