SQLAlchemy one-to-one, store foreign key in each table? - python

I have a relationship that is one to one between cage codes and duns numbers.
I have set up my relationship that looks like, where I store a ForeignKey on each of the respective tables.
class Cage(Base):
__tablename__ = 'DimCage'
id = Column(Integer, primary_key=True)
cage = Column(String(8), unique=True, nullable=False)
duns_id = Column(Integer, ForeignKey('DimDuns.id'))
duns = relationship('Duns', uselist=False, back_populates='cage')
class Duns(Base):
__tablename__ = 'DimDuns'
id = Column(Integer, primary_key=True)
duns = Column(String(10), unique=True, nullable=False)
dunsInt = Column(Integer, unique=True, nullable=False)
cage_id = Column(Integer, ForeignKey('DimCage.id'))
cage = relationship('Cage', uselist=False, back_populates='duns')
When I create the tables I get the below error, how to do I set up my foreign keys so I can keep a reference on both tables?
sqlalchemy.exc.AmbiguousForeignKeysError: Can't determine join between 'DimCage' and 'DimDuns'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly.
And During handling of the above exception, another exception occurred:
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Cage.duns - 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 believe you only need to store one foreign key for a one-to-one relationship.
See https://docs.sqlalchemy.org/en/13/orm/basic_relationships.html#one-to-one
You shouldn't lose any data access this way. If you remove the duns_id column from Cage, you now access the id by cage.duns.id instead of cage.duns_id.

You could set the child primary key as a foreign key to the parent.
For a design perspective, it is not a bad choice to use foreign keys as primary keys in One-to-One relationships.
class Cage(Base):
__tablename__ = 'DimCage'
id = Column(Integer, primary_key=True)
cage = Column(String(8), unique=True, nullable=False)
class Duns(Base):
__tablename__ = 'DimDuns'
id = Column(Integer, ForeignKey(Cage.id) primary_key=True)
duns = Column(String(10), unique=True, nullable=False)
dunsInt = Column(Integer, unique=True, nullable=False)
cage = relationship('Cage', uselist=False, backref='duns')
Now both cage and duns have the same id. So:
session.add(Cage(id=1, duns=Duns(duns='Duns', dunsInt=10)))
sesion.commit()
id = 1
cage = select(Cage).where(Cage.id == id)
duns = select(Duns).where(Duns.id == cage.id)
assert cage.id == duns.id
Please note that the child cannot exist without the parent.
If the parent is going to be deleted then the child must be deleted first, unless you configure some cascade option.

Related

Many to many relationship in SQLAlchemy with same foreign key used twice in association object

I have three tables - Competition, Competitor, and Duel. Table Duel is actually an association object between tables Competition and Competitor. The code is following:
class Competition(db.Model):
__tablename__ = 'competitions'
id_competition = db.Column(db.Integer, primary_key=True, autoincrement=True)
# some additional attributes
competitors_duel = relationship('Duel', back_populates='rel_competition')
class Competitor(db.Model):
__tablename__ = 'competitors'
id_competitor = db.Column(db.Integer, primary_key=True, autoincrement=True)
# some additional attributes
competitions_duel = relationship('Duel', back_populates='rel_competitor')
class Duel(db.Model):
__tablename__ = 'duels'
id_competitor1 = db.Column(db.ForeignKey('competitors.id_competitor'), primary_key=True)
id_competitor2 = db.Column(db.ForeignKey('competitors.id_competitor'), primary_key=True)
id_competition = db.Column(db.ForeignKey('competitions.id_competition'), primary_key=True)
phase = db.Column(db.Integer, primary_key=True)
# some additional attributes
rel_competitor = relationship('Competitor', back_populates='competitions_duel')
rel_competition = relationship('Competition', back_populates='competitors_duel')
This is the same association object as example of bidirectional association object in documentation.
Problem that I have is this:
Could not determine join condition between parent/child tables on
relationship Competitor.competitions_duel - 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.
The error is gone when I delete id_competitor2 (or 1). But I need both ids because it is a duel, I need to know both competitors. Also, each contest has its phase so those four keys (id_competitor1, id_competitor2, id_competition, and phase) should actually create one composite key which differentiates between duels.
How can I solve this?

How can I query specific child in multiple-level relationship table in SQLAlchemy

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)

SQLAlchemy AssertionError: Dependency rule tried to blank-out primary key column

because of the following line: contact_person.selections_to_persons[selection_id].post_address = address
i get the following error at the next commit:
AssertionError: Dependency rule tried to blank-out primary key column 'selections_to_persons.t_person_to_department_id' on instance ''
The important parts of the involved models are:
class SelectionToPerson(OrmModelBase, TableModelBase):
__tablename__ = 'selections_to_persons'
__table_args__ = (
ForeignKeyConstraint(
["address_tablename",
"address_id",
"t_person_to_department_id"],
["address_collection.tablename",
"address_collection.id",
"address_collection.t_person_to_department_id"],
name="fk_post_address_selection_to_person", use_alter=True
),
)
selection_id = Column(Integer,
ForeignKey('selections.selection_id',
onupdate=NO_ACTION,
ondelete=CASCADE),
primary_key=True, nullable=False)
t_person_to_department_id = Column(
Integer,
ForeignKey('t_persons_to_departments.t_person_to_department_id',
onupdate=NO_ACTION,
ondelete=CASCADE),
primary_key=True,
nullable=False)
address_tablename = Column(String, nullable=False)
address_id = Column(Integer, nullable=False)
post_address = relationship(AddressCollection)
class AddressCollection(OrmModelBase, ViewModelBase):
__tablename__ = 'address_collection'
tablename = Column(String, primary_key=True)
id = Column(Integer, primary_key=True)
t_person_to_department_id = Column(
Integer,
ForeignKey('t_persons_to_departments.t_person_to_department_id'),
primary_key=True)
Does anyone know why this error occurs?
One of the cases when this error occurs is an attempt to assign null to a field that is a primary key.
You have several primary keys that are specified by foreign keys.
I don't know for sure, but it is possible that the expression contact_person.selections_to_persons[selection_id].post_address = address created an object with null reference. That is, after assignment, some object remains with a null reference.
I am leaving a few links that describe how to use cascades in different cases. This might help those who get this error.
This is how cascades work:
https://docs.sqlalchemy.org/en/13/orm/cascades.html#unitofwork-cascades
Here's how you can configure cascades using the example of deleting: https://docs.sqlalchemy.org/en/13/orm/tutorial.html#configuring-delete-delete-orphan-cascade

SQLalchemy-Flask: ArgumentError for one to many relationship

I am trying to use sqlalchemy to run queries for one to many relationship. I am having trouble getting my queries to run.
class Quote(db.Model):
id = db.Column(db.Integer, primary_key=True)
description = db.Column(db.String(1000))
category = db.Column(db.String(100))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
date_added = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
quote_cat = db.relationship("Quote", backref='category', lazy=True)
quote_id_ = db.Column(db.Integer, db.ForeignKey('quote.id'))
sqlalchemy.exc.ArgumentError: Mapper mapped class Category->category
could not assemble any primary key columns for mapped table 'category'
Your quote_cat backref references a property that already exists on the Quote class. Either remove this or change the backref value.
Here are the backref docs:
backref –
indicates the string name of a property to be placed on the related mapper’s class that will handle this relationship in the other direction

SQLAlchemy Handling Multiple Paths In One Relationship

Please note: this question is related but separate from my other currently open question SQLAlchemy secondary join relationship on multiple foreign keys.
The SQLAlchemy documentation describes handling multiple join paths in a single class for multiple relationships:
from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Customer(Base):
__tablename__ = 'customer'
id = Column(Integer, primary_key=True)
name = Column(String)
billing_address_id = Column(Integer, ForeignKey("address.id"))
shipping_address_id = Column(Integer, ForeignKey("address.id"))
billing_address = relationship("Address")
shipping_address = relationship("Address")
class Address(Base):
__tablename__ = 'address'
id = Column(Integer, primary_key=True)
street = Column(String)
city = Column(String)
state = Column(String)
zip = Column(String)
Within the same section the documentation shows three separate ways to define the relationship:
billing_address = relationship("Address", foreign_keys=[billing_address_id])
billing_address = relationship("Address", foreign_keys="[Customer.billing_address_id]")
billing_address = relationship("Address", foreign_keys="Customer.billing_address_id")
As you can see in (1) and (2) SQLAlchemy allows you to define a list of foreign_keys. In fact, the documentation explicitly states:
In this specific example, the list is not necessary in any case as there’s only one Column we need: billing_address = relationship("Address", foreign_keys="Customer.billing_address_id")
But I cannot determine how to use the list notation to specify multiple foreign keys in a single relationship.
For the classes
class PostVersion(db.Model):
id = db.Column(db.Integer, primary_key=True)
...
tag_1_id = db.Column(db.Integer, db.ForeignKey("tag.id"))
tag_2_id = db.Column(db.Integer, db.ForeignKey("tag.id"))
tag_3_id = db.Column(db.Integer, db.ForeignKey("tag.id"))
tag_4_id = db.Column(db.Integer, db.ForeignKey("tag.id"))
tag_5_id = db.Column(db.Integer, db.ForeignKey("tag.id"))
class Tag(db.Model):
id = db.Column(db.Integer, primary_key=True)
tag = db.Column(db.String(127))
I have tried all of the following:
tags = db.relationship("Tag", foreign_keys=[tag_1_id, tag_2_id, tag_3_id, tag_4_id, tag_5_id]) resulting in
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship AnnotationVersion.tags - 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.
tags = db.relationship("Tag", foreign_keys="[tag_1_id, tag_2_id, tag_3_id, tag_4_id, tag_5_id]") resulting in
sqlalchemy.exc.InvalidRequestError: When initializing mapper Mapper|AnnotationVersion|annotation_version, expression '[tag_1_id, tag_2_id, tag_3_id, tag_4_id, tag_5_id]' failed to locate a name ("name 'tag_1_id' is not defined"). If this is a class name, consider adding this relationship() to the class after both dependent classes have been defined.
And many others variations on the list style, using quotes inside and outside, using Table names and Class names.
I've actually solved the problem in the course of this question. Since there seems to be no direct documentation, I'll answer it myself instead of deleting this question.
The key is to define the relationship on a primary join and specify the uselist parameter.
tags = db.relationship("Tag", primaryjoin="or_(PostVersion.tag_1_id==Tag.id,"
"PostVersion.tag_2_id==Tag.id, PostVersion.tag_3_id==Tag.id,"
"PostVersion.tag_4_id==Tag.id, PostVersion.tag_5_id==Tag.id)",
uselist=True)

Categories

Resources