I'm trying to accomplish the following joined inheritance with one-to-one relationship structure with SQLAlchemy: Box and Item are both Elements, and Box has an Item.
The class definitions are as follows:
class Element(Base):
__tablename__ = 'element'
id = Column(Integer, primary_key=True)
el_type = Column(String)
__mapper_args__ = dict(
polymorphic_identity='element',
polymorphic_on=el_type,
)
class Box(Element):
# Joined table inheritance: Element.
__tablename__ = 'box'
id = Column(Integer, ForeignKey('element.id'), primary_key=True)
__mapper_args__ = dict(polymorphic_identity='box')
# One-to-one relationship: Item.
item = relationship('Item',
back_populates='box', uselist=False)
def __init__(self, item_name=None):
self.item = Item(item_name)
class Item(Element):
# Joined table inheritance: Element.
__tablename__ = 'item'
id = Column(Integer, ForeignKey('element.id'), primary_key=True)
__mapper_args__ = dict(polymorphic_identity='item')
# One-to-one relationship: Box.
box_id = Column(Integer, ForeignKey('box.id'))
box = relationship('Box',
back_populates='item')
name = Column(String)
def __init__(self, name):
self.name = name
When I try to instantiate b = Box('rock'), I get:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Box.item - 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.
Now I have gone to SQLAlchemy's page on this issue (great docs, I might add), but I'm not sure how to bridge the gap between their example and my situation. I thought this might fix it:
class Item(Element):
# ...
box = relationship('Box',
back_populates='item', foreign_keys=[id, box_id])
# ...
...but I get the same error.
What am I doing wrong?
UPDATE:
I have now tried using ForeignKeyConstraint constructs in an effort to more explicitly describe the desired behavior to SQLAlchemy; to no avail:
class Box(Element):
# ...
id = Column(Integer, primary_key=True)
# ...
ForeignKeyConstraint([id], ['element.id'])
# ...
class Item(Element):
# ...
id = Column(Integer, primary_key=True)
# ...
# One-to-one relationship: Box.
box_id = Column(Integer)
box = relationship('Box',
back_populates='item')
ForeignKeyConstraint([id, box_id], ['element.id', 'box.id'])
# ...
A simple AssertionError is thrown back at me (no description). Given that SQLAlchemy has a good track record of producing meaningful error messages, could this suggest that this is an unanticipated situation?
Related
I'm trying to implement a user-facing PreviewList of Articles, which will keep its size even if an Article is deleted. So if the list has four objects [1, 2, 3, 4] and one is deleted, I want it to contain [1, 2, None, 4].
I'm using a relationship with a secondary table. Currently, deleting either Article or PreviewList will delete the row in that table. I've experimented with cascade options, but they seem to affect the related items directly, not the contents of the secondary table.
The snippet below tests for the desired behaviour: deleting an Article should preserve the row in ArticlePreviewListAssociation, but deleting a PreviewList should delete it (and not the Article).
In the code below, deleting the Article will preserve the ArticlePreviewListAssociation, but pl.articles does not treat that as a list entry.
from db import DbSession, Base, init_db
from sqlalchemy import Column, String, Integer, ForeignKey
from sqlalchemy.orm import relationship
session = DbSession()
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
class PreviewList(Base):
__tablename__ = 'preview_lists'
id = Column(Integer, primary_key=True)
articles = relationship('Article', secondary='associations')
class ArticlePreviewListAssociation(Base):
__tablename__ = 'associations'
article_id = Column(Integer, ForeignKey('articles.id'), nullable=True)
previewlist_id = Column(Integer, ForeignKey('preview_lists.id'), primary_key=True)
article = relationship('Article')
preview_list = relationship('PreviewList')
init_db()
print(f"Creating test data")
a = Article(title="StackOverflow: 'Foo' not setting 'Bar'?")
pl = PreviewList(articles=[a])
session.add(a)
session.add(pl)
session.commit()
print(f"ArticlePreviewListAssociations: {session.query(ArticlePreviewListAssociation).all()}")
print(f"Deleting PreviewList")
session.delete(pl)
associations = session.query(ArticlePreviewListAssociation).all()
print(f"ArticlePreviewListAssociations: should be empty: {associations}")
if len(associations) > 0:
print("FAIL")
print("Reverting transaction")
session.rollback()
print("Deleting article")
session.delete(a)
articles_in_list = pl.articles
associations = session.query(ArticlePreviewListAssociation).all()
print(f"ArticlePreviewListAssociations: should not be empty: {associations}")
if len(associations) == 0:
print("FAIL")
print(f"Articles in PreviewList: should not be empty: {articles_in_list}")
if len(articles_in_list) == 0:
print("FAIL")
# desired outcome: pl.articles should be [None], not []
print("Reverting transaction")
session.rollback()
This may come down to "How can you make a many-to-many relationship where pk_A == 1 and pk_B == NULL include the None in A's list?"
The given examples would seem to assume that the order of related articles is preserved, even upon deletion. There are multiple approaches to that, for example the Ordering List extension, but it is easier to first solve the problem of preserving associations to deleted articles. This seems like a use case for an association object and proxy.
The Article class gets a new relationship so that deletions cascade in a session. The default ORM-level cascading behavior is to set the foreign key to NULL, but if the related association object is not loaded, we want to let the DB do it, so passive_deletes=True is used:
class Article(Base):
__tablename__ = 'articles'
id = Column(Integer, primary_key=True)
title = Column(String)
previewlist_associations = relationship(
'ArticlePreviewListAssociation', back_populates='article',
passive_deletes=True)
Instead of a many to many relationship PreviewList uses the association object pattern, along with an association proxy that replaces the many to many relationship. This time the cascades are a bit different, since the association object should be deleted, if the parent PreviewList is deleted:
class PreviewList(Base):
__tablename__ = 'preview_lists'
id = Column(Integer, primary_key=True)
article_associations = relationship(
'ArticlePreviewListAssociation', back_populates='preview_list',
cascade='all, delete-orphan', passive_deletes=True)
articles = association_proxy(
'article_associations', 'article',
creator=lambda a: ArticlePreviewListAssociation(article=a))
Originally the association object used previewlist_id as the primary key, but then a PreviewList could contain a single Article only. A surrogate key solves that. The foreign key configurations include the DB level cascades. These are the reason for using passive deletes:
class ArticlePreviewListAssociation(Base):
__tablename__ = 'associations'
id = Column(Integer, primary_key=True)
article_id = Column(
Integer, ForeignKey('articles.id', ondelete='SET NULL'))
previewlist_id = Column(
Integer, ForeignKey('preview_lists.id', ondelete='CASCADE'),
nullable=False)
# Using a unique constraint on a nullable column is a bit ugly, but
# at least this prevents inserting an Article multiple times to a
# PreviewList.
__table_args__ = (UniqueConstraint(article_id, previewlist_id), )
article = relationship(
'Article', back_populates='previewlist_associations')
preview_list = relationship(
'PreviewList', back_populates='article_associations')
With these changes in place no "FAIL" is printed.
I am trying to create an edge type class to link two nodes. Each of the nodes is a class by itself and both nodes and edge should be part of the same graph. I keep getting
Could not determine join condition between parent/child tables on relationship TypeiNode.edges - there are no foreign keys linking these tables.
even though the foreign keys are defined. I tried defining foreign keys on the node table edges relationship but got the same result. Can someone tell me what is wrong with this example.
class Edge(Base):
__tablename__ = 'edge'
id = Column(Integer, primary_key=True)
graph_id = Column(Integer, ForeignKey('graph.id'))
graph= relationship('Graph', back_populates='graph_edges')
type_1_node_id = Column(Integer, ForeignKey('type_1_node.id'))
type_1_node = relationship('Type1Node',
back_populates='edges',
foreign_keys=[type_1_node_id, graph_id])
type_2_node_id = Column(Integer, ForeignKey('type_2_node.id'))
type_2_node = relationship('Type2Node',
back_populates='edges',
foreign_keys=[type_2_node_id, graph_id ])
__table_args__ = (
ForeignKeyConstraint(
['type_1_node_id', 'graph_id'],
['type_1_node.id','type_1_node.graph_id']),
ForeignKeyConstraint(
['type_2_node_id', 'graph_id'],
['type_2_node.id','type_2_node.graph_id']),
)
class Type1Node(Base):
__tablename__ = 'type_1_node'
id = Column(Integer, primary_key=True)
graph_id = Column(Integer, ForeignKey('graph.id'))
graph = relationship('Graph', back_populates='graph_1_nodes')
edges = relationship('Edge', back_populates='type_1_node')
class Type2Node(Base):
__tablename__ = 'type_2_node'
id = Column(Integer, primary_key=True)
graph_id = Column(Integer, ForeignKey('graph.id'))
graph = relationship('Graph', back_populates='graph_2_nodes')
edges = relationship('Edge', back_populates='type_2_node')
If you read the error carefully, you'll notice it's not complaining about the relationships that you added the foreign_keys argument to, but the ones you didn't:
Could not ... on relationship **TypeiNode.edges** ...
I take
I tried defining foreign keys on the node table edges relationship but got the same result.
to mean that you did not try defining both ends at the same time. So the solution is to define the foreign keys in both ends:
class Type1Node(Base):
__tablename__ = 'type_1_node'
id = Column(Integer, primary_key=True)
graph_id = Column(Integer, ForeignKey('graph.id'))
graph = relationship('Graph')
edges = relationship('Edge', back_populates='type_1_node',
foreign_keys=[Edge.type_1_node_id, Edge.graph_id])
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")
I'm using SQLAlchemy to represent a relationship between authors. I'd like to have authors related to other authors (coauthorshp), with extra data in the relation, such that with an author a I can find their coauthors.
How this is done between two different objects is this:
class Association(Base):
__tablename__ = 'association'
left_id = Column(Integer, ForeignKey('left.id'), primary_key=True)
right_id = Column(Integer, ForeignKey('right.id'), primary_key=True)
extra_data = Column(String(80))
child = relationship('Child', backref='parent_assocs')
class Parent(Base):
__tablename__ = 'left'
id = Column(Integer, primary_key=True)
children = relationship('Association', backref='parent')
class Child(Base):
__tablename__ = 'right'
id = Column(Integer, primary_key=True)
but how would I do this in my case?
The nature of a coauthorship is that it is bidirectional. So, when you insert the tuple (id_left, id_right) into the coauthorship table through a coauthoship object, is there a way to also insert the reverse relation easily? I'm asking because I want to use association proxies.
if you'd like to literally have pairs of rows in association, that is, for every id_left, id_right that's inserted, you also insert an id_right, id_left, you'd use an attribute event to listen for append events on either side, and produce an append in the other direction.
If you just want to be able to navigate between Parent/Child in either direction, just a single row of id_left, id_right is sufficient. The examples in the docs regarding this kind of mapping illustrate the whole thing.
My Python / SQLAlchemy application manages a set of nodes, all derived from a base class Node. I'm using SQLAlchemy's polymorphism features to manage
the nodes in a SQLite3 table. Here's the definition of the base Node class:
class Node(db.Base):
__tablename__ = 'nodes'
id = Column(Integer, primary_key=True)
node_type = Column(String(40))
title = Column(UnicodeText)
__mapper_args__ = {'polymorphic_on': node_type}
and, as an example, one of the derived classes, NoteNode:
class NoteNode(Node):
__mapper_args__ = {'polymorphic_identity': 'note'}
__tablename__ = 'nodes_note'
id = Column(None,ForeignKey('nodes.id'),primary_key=True)
content_type = Column(String)
content = Column(UnicodeText)
Now I need a new kind of node, ListNode, that is an ordered container of zero or more Nodes. When I load a ListNode, I want it to have its ID and title
(from the base Node class) along with a collection of its contained (child) nodes. A Node may appear in more than one ListNode, so it's not a proper hierarchy. I would create them along these lines:
note1 = NoteNode(title=u"Note 1", content_type="text/text", content=u"I am note #1")
session.add(note1)
note2 = NoteNode(title=u"Note 2", content_type="text/text", content=u"I am note #2")
session.add(note2)
list1 = ListNode(title=u"My List")
list1.items = [note1,note2]
session.add(list1)
The list of children should only
consist of Node objects -- that is, all I need is their base class stuff. They shouldn't be fully realized into the specialized classes
(so I don't get the whole graph at once, among other reasons).
I started along the following lines, cobbling together bits and pieces I found in various places without a complete understanding of
what was going on, so this may not make much sense:
class ListNode(Node):
__mapper_args__ = {'polymorphic_identity': 'list', 'inherit_condition':id==Node.id}
__tablename__ = 'nodes_list_contents'
id = Column(None, ForeignKey('nodes.id'), primary_key=True)
item_id = Column(None, ForeignKey('nodes.id'), primary_key=True)
items = relation(Node, primaryjoin="Node.id==ListNode.item_id")
This approach fails in several ways: it doesn't appear to allow an empty ListNode, and setting the items attribute to a list results
in SQLAlchemy complaining that 'list' object has no attribute '_sa_instance_state'. Not surprisingly, hours of random mutations on this theme haven't given any
good results,
I have limited experience in SQLAlchemy but really want to get this working soon. I'd very much appreciate any advice or direction you can
offer. Thanks in advance!
You need an additional table for many-to-many relation:
nodes_list_nodes = Table(
'nodes_list_nodes', metadata,
Column('parent_id', None, ForeignKey('nodes_list.id'), nullable=False),
Column('child_id', None, ForeignKey(Node.id), nullable=False),
PrimaryKeyConstraint('parent_id', 'child_id'),
)
class ListNode(Node):
__mapper_args__ = {'polymorphic_identity': 'list'}
__tablename__ = 'nodes_list'
id = Column(None, ForeignKey('nodes.id'), primary_key=True)
items = relation(Node, secondary=nodes_list_nodes)
Update: below is an example for ordered list using association_proxy:
from sqlalchemy.orm.collections import InstrumentedList
from sqlalchemy.ext.associationproxy import association_proxy
class ListNodeAssociation(Base):
__tablename__ = 'nodes_list_nodes'
parent_id = Column(None, ForeignKey('nodes_list.id'), primary_key=True)
child_id = Column(None, ForeignKey(Node.id), primary_key=True)
order = Column(Integer, nullable=False, default=0)
child = relation(Node)
__table_args__ = (
PrimaryKeyConstraint('parent_id', 'child_id'),
{},
)
class OrderedList(InstrumentedList):
def append(self, item):
if self:
item.order = self[-1].order+1
else:
item.order = 1
InstrumentedList.append(self, item)
class ListNode(Node):
__mapper_args__ = {'polymorphic_identity': 'list'}
__tablename__ = 'nodes_list'
id = Column(None, ForeignKey('nodes.id'), primary_key=True)
_items = relation(ListNodeAssociation,
order_by=ListNodeAssociation.order,
collection_class=OrderedList,
cascade='all, delete-orphan')
items = association_proxy(
'_items', 'child',
creator=lambda item: ListNodeAssociation(child=item))