I have a polymorphic association set up like below:
class Parent(Base)
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
class Child(Parent)
parent_id = Column(ForeignKey(Parent.id), primary_key=True)
description = Column(String)
__mapper_args__ = {
'polymorphic_identity': 'child'
}
I would like to be able to first create the parent table, and then link it to the child table like so:
parent = Parent(name='foobar')
session.add(parent)
session.flush()
child = Child(description='baz', parent_id=parent.id)
However I receive an integrity error on the name field null constraint on the child creation - meaning that last line attempts to insert into the parent table as well.
Is there a way to achieve this split inserts?
Related
I have a 3-level database, tables such as:
class GrandParent(Base):
__tablename__ = 'grandparent'
id = Column(Integer, primary_key=True)
field0 = Column(Float)
parents = relationship("Parent", back_populates="parent", cascade="all, delete-orphan")
class Parent(Base):
__tablename__ = 'parents'
id = Column(Integer, primary_key=True)
field0 = Column(Float)
parent_id = Column("parent", Integer, ForeignKey("grandparent.id"))
parent = relationship("GrandParent", back_populates="parents")
children = relationship("Child", back_populates="parent", cascade="all, delete-orphan")
class Child(Base):
__tablename__ = 'children'
id = Column(Integer, primary_key=True)
field0 = Column(Float)
parent_id = Column("parent", Integer, ForeignKey("parents.id"))
parent = relationship("Parent", back_populates="children")
I stored a record for GrandParent that holds N records of Parent, each handling M records of Child. I need to modify GrandParent instance structure such that it is convenient to erase all its children and grand-children and rebuild the tree. I have to perform these modifications in memory, before letting the user save (persist) changes to the database.
I tried:
for parent in grand_parent.children:
for child in parent.children:
session.delete(child)
session.delete(parent)
# Other code here to add children/ gran-children from scratch to the grand_parent instance
This gives strange behaviour: the new instances are replicated along with the old ones attached to grand_parent.
So I tried force in-class-object deletion:
for parent in grand_parent.children:
parent.children.clear()
grand_parent.children.clear()
Does not work either, with the same result (even if I join the two methods and delete both class-object and from session). How to handle this?
I have the multiple-level one-to-many tables, linked by the foreign key.
I frequently query the specific child according to family_id, grandparents, and parent's name.
The query result should be only one.
If a child does not exist, I'll create a new child record and link it to the given parent.
The number of the child table is much larger than the parent table.
Childs >>>> Parents >> Grandparents > families
People from different families can have the same name.
(The name in the child table can be the same because they might come from different families and different parents)
Here are the model definitions
class Families:
__tablename__ = 'families'
id = Column(Integer, primary_key=True)
family_name = Column(String, nullable=False)
grand_parents = relationship(GrandParents, backref="family")
class GrandParents:
__tablename__ = 'grand_parents'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
family_id = Column(Integer, ForeignKey("families.id"))
parents = relationship(Parents, backref="grand_parent")
class Parents:
__tablename__ = 'parents'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
grand_parent_id = Column(Integer, ForeignKey("grand_parents.id"))
childs = relationship(Childs, backref="parent")
class Childs:
__tablename__ = 'childs'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
parent_id = Column(Integer, ForeignKey("parents.id"))
For now, I use join to query the targeted child row by given family_id, grandparent's name, and parent's name
def get_child(family_id, grand_praent_name, parent_name, child_name):
child = session.query(Childs)\
.join(Parents)\
.join(GrandParents)\
.filter(GrandParents.family_id == family_id,
GrandParents.name == grand_praent_name,
Parents.name == parent_name,
Childs.name == child_name).one_or_none()
return child
But each time, I have to do this kind of query (go through all children for a specific family and update their value depending on business logic).
Is there a better approach/design/idiomatic to do this kind of query?
Your query looks good, if you only update child. You may consider using with_entities to avoid fetching to many unnecessary data:
def get_child(family_id, grand_praent_name, parent_name, child_name):
child = session.query(Childs)\
.join(Parents)\
.join(GrandParents)\
.filter(GrandParents.family_id == family_id,
GrandParents.name == grand_praent_name,
Parents.name == parent_name,
Childs.name == child_name)\
.with_entities(Childs).one_or_none()
return child
If you would like to update also parent or grandparent you should use joinedload to avoid additional implicit queries.
If you encountered performance issues for your query you should add indexes to foreign keys to improve joining, e.g.:
parent_id = Column(Integer, ForeignKey("parents.id"), index=True)
Two declarative classes which have a parent child relationship, the youngest child is the most important child and thus a youngest_child_id column would be useful.
In this there are two relationships - a one to one from parent to child and a one to many from parent to children, but this creates multiple join paths
Something like the below:
class Parent(Base):
__tablename__ = 'parents'
id = Column(Integer, primary_key=True)
youngest_child_id = Column(Integer, foreign_key='Child.id')
youngest_child = relationship("Child", uselist=False, foreign_keys=[youngest_child_id])
children = relationship("Child", back_populates='parent')
Class Child(Base):
__tablename__ = 'children'
id = id = Column(Integer, primary_key=True)
parent_id = Column(Integer, foreign_key='Parent.id')
parent = relationship("Parent", back_populates='children')
This and a few other variations that I have created raise AmbiguousForeignKeysError:
Exception has occurred: sqlalchemy.exc.AmbiguousForeignKeysError
Could
not determine join condition between parent/child tables on
relationship Parent.children
Where is this going wrong and can this be achieved via the ORM?
You've defined foreign_keys for the youngest_child relationship, but you also have to define it for the children and parent relationships:
class Parent(Base):
__tablename__ = 'parents'
id = Column(Integer, primary_key=True)
youngest_child_id = Column(Integer, ForeignKey('children.id'))
youngest_child = relationship("Child", uselist=False, post_update=True,
foreign_keys=[youngest_child_id])
# Pass foreign_keys= as a Python executable string for lazy evaluation
children = relationship("Child", back_populates='parent',
foreign_keys='[Child.parent_id]')
class Child(Base):
__tablename__ = 'children'
id = id = Column(Integer, primary_key=True)
parent_id = Column(Integer, ForeignKey('parents.id'))
parent = relationship("Parent", back_populates='children',
foreign_keys=[parent_id])
In addition you must define post_update=True on for example youngest_child in order to break the circular dependency between the models. Without it SQLAlchemy would have to insert both the parent and the child at the same time, if you do something like this:
p = Parent()
c1, c2 = Child(), Child()
p.children = [c1, c2]
p.youngest_child = c1
session.add(p)
session.commit()
With the post update in place SQLAlchemy first inserts to parents, then to children, and then updates the parent with the youngest child.
Is there any way to create a relationship like this (example data) between Parent and Child based on parent_id and id respectively:
Parent
parent_id: "A1234"
name: "Parent Name"
Child
id: 1234
how can I add the foreign key to the Child? The parent_id is a String. Is there a way to slice it and then cast to Integer?
Edit:
Also what if the situation happens other way round:
Child:
child_id: "A1234"
Parent:
parent_letter: "A"
parent_id: 1234
would that be something like:
primaryjoin=(child_id == (Parent.parent_letter + str(Parent.parent_id)))
what would the remote_side look like? or the entire relationship?
See Creating Custom Foreign Conditions section of documentation. Using the cast, the relationship can be setup for the model below:
class Parent(Base):
__tablename__ = 'parent'
parent_id = Column(String, primary_key=True)
name = Column(String, nullable=False)
class Child(Base):
__tablename__ = 'child'
id = Column(Integer, primary_key=True)
name = Column(String, nullable=False)
parent = relationship(
Parent,
primaryjoin=("A" + cast(id, String) == Parent.parent_id),
foreign_keys=id,
remote_side=Parent.parent_id,
backref="children",
# uselist=False, # use in case of one-to-one relationship
)
In this case you can query for Parent.children or Child.parent:
p1 = session.query(Parent).get('A1234')
print(p1)
print(p1.children)
c1 = session.query(Child).get(1234)
print(c1)
print(c1.parent)
However you would still not be able to create relationship items like below:
p = Parent(
parent_id='A3333', name='with a child',
children=[Child(name='will not work')]
)
session.add(p)
session.commit() # this will fail
Edit-1: For the alternative case you mention in your comments and edit, following relationship definition should work (obviously, the model is defined differently as well):
parent = relationship(
Parent,
primaryjoin=(
foreign(child_id) ==
remote(Parent.parent_letter + cast(Parent.parent_id, String))
),
backref="children",
uselist=False,
)
My table relationships in SQLAlchemy have gotten quite complex, and now I'm stuck at this error no matter how I configure my relationship.
I'm a bit new to SQLAlchemy so I'm not sure what I'm doing wrong, but I keep getting the same error no matter what I do.
I have a hierarchy of tables, all inheriting from 'Node', which is a table with self-referential id and parent_id columns. All of this works. Now I have another Node table 'Information', which contains a composite primary key that is referenced by a different Node table, 'Widget'.
Base = declarative_base()
class Node(Base):
__tablename__ = 'Node'
id = Column(Integer, primary_key=True)
parentid = Column(Integer, ForeignKey('Node.ID')
type = Column(Text(50))
children = relationship('Node')
__mapper_args__ = {
'polymorphic_identity': 'Node',
'polymorphic_on': type
}
class Information(Node):
__tablename__ = 'Information'
id = Column(Integer, ForeignKey('Node.ID'), primary_key=True)
Name = Column(Text, primary_key=True)
Number = Column(Float, primary_key=True)
Widgets = relationship('Widget', backref='ThisInformation')
__mapper_args__ = {'polymorphic_identity': 'Information'}
class Widget(Node):
__tablename__ = 'Widget'
id = Column(Integer, ForeignKey('Node.ID'), primary_key=True)
Name = Column(Text)
UnitType = Column(Text)
Amount = Column(Float)
_Number = Column('Number', Float)
__table_args__ = (ForeignKeyConstraint(
['Name', 'Number'],
['Information.Name', 'Information.Number']),
{})
__mapper_args__ = {'polymorphic_identity': 'Widget'}
I was worried this would give me circular reference issues, but instead it gives me this error:
InvalidRequestError: One or more mappers failed to initialize - can't proceed with initialization of other mappers. Original exception was: Could not determine join condition between parent/child tables on relationship Widget.Information - 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.
I have tried adding foreign_keys arguments to the relationship, both on Widget and Information side, but I get the exact same error. Can anyone help?
After a lot of searching I finally found a really simple answer in this answer to another question.
All I did was add 'inherit_condition': (id == Node.id) to the mapper in the classes that inherit from Node so that it looked like this:
__mapper_args__ = {'polymorphic_identity': 'Information',
'inherit_condition': (id == Node.id)}
And it works perfectly.