Follow

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use
Contact

Using `sqlalchemy.orm` with composite primary keys

Using sqlalchemy.orm I am trying to link two tables on a composite key, but keep getting an error. Unfortunately, the official docs provide an example that uses a single primary key (not composite), so I tried to come up with a basic example that reproduces the issue:

from sqlalchemy import Column, ForeignKey, Integer, String
from sqlalchemy.orm import DeclarativeBase, mapped_column, relationship


class Base(DeclarativeBase):
    pass


class Customer(Base):
    __tablename__ = "customer"
    id = mapped_column(String, primary_key=True)
    country = mapped_column(String, primary_key=True)

    billing_address_id = mapped_column(Integer, ForeignKey("address.idx"))
    shipping_address_id = mapped_column(Integer, ForeignKey("address.idx"))

    billing_address = relationship("Address", foreign_keys=[billing_address_id])
    shipping_address = relationship("Address", foreign_keys=[shipping_address_id])


class Address(Base):
    __tablename__ = "address"
    idx = mapped_column(Integer, primary_key=True)
    address = mapped_column(String)
    customer_id = mapped_column(ForeignKey("customer.id"))
    customer_country = mapped_column(ForeignKey("customer.country"))
    customers_using_this_adress = relationship(
        "Customer", foreign_keys=[customer_id, customer_country]
    )

# trying to define a customer triggers an error
c = Customer(id="A", country="B")

I get the following error:

AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Address.customers_using_this_adress - 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.

Here’s the full traceback:

MEDevel.com: Open-source for Healthcare and Education

Collecting and validating open-source software for healthcare, education, enterprise, development, medical imaging, medical records, and digital pathology.

Visit Medevel

---------------------------------------------------------------------------
AmbiguousForeignKeysError                 Traceback (most recent call last)
File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py:2421, in JoinCondition._determine_joins(self)
   2420 if self.primaryjoin_initial is None:
-> 2421     self.primaryjoin = join_condition(
   2422         self.parent_persist_selectable,
   2423         self.child_persist_selectable,
   2424         a_subset=self.parent_local_selectable,
   2425         consider_as_foreign_keys=consider_as_foreign_keys,
   2426     )
   2427 else:

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/sql/util.py:123, in join_condition(a, b, a_subset, consider_as_foreign_keys)
    101 """Create a join condition between two tables or selectables.
    102 
    103 e.g.::
   (...)
    121 
    122 """
--> 123 return Join._join_condition(
    124     a,
    125     b,
    126     a_subset=a_subset,
    127     consider_as_foreign_keys=consider_as_foreign_keys,
    128 )

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py:1356, in Join._join_condition(cls, a, b, a_subset, consider_as_foreign_keys)
   1355 if len(constraints) > 1:
-> 1356     cls._joincond_trim_constraints(
   1357         a, b, constraints, consider_as_foreign_keys
   1358     )
   1360 if len(constraints) == 0:

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py:1501, in Join._joincond_trim_constraints(cls, a, b, constraints, consider_as_foreign_keys)
   1500 if len(constraints) != 1:
-> 1501     raise exc.AmbiguousForeignKeysError(
   1502         "Can't determine join between '%s' and '%s'; "
   1503         "tables have more than one foreign key "
   1504         "constraint relationship between them. "
   1505         "Please specify the 'onclause' of this "
   1506         "join explicitly." % (a.description, b.description)
   1507     )

AmbiguousForeignKeysError: Can't determine join between 'address' and 'customer'; tables have more than one foreign key constraint relationship between them. Please specify the 'onclause' of this join explicitly.

The above exception was the direct cause of the following exception:

AmbiguousForeignKeysError                 Traceback (most recent call last)
Cell In[11], line 32
     26     customer_country = mapped_column(ForeignKey("customer.country"))
     27     customers_using_this_adress = relationship(
     28         "Customer", foreign_keys=[customer_id, customer_country]
     29     )
---> 32 c = Customer(id="A", country="B")

File <string>:4, in __init__(self, **kwargs)

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/state.py:570, in InstanceState._initialize_instance(*mixed, **kwargs)
    567 self, instance, args = mixed[0], mixed[1], mixed[2:]  # noqa
    568 manager = self.manager
--> 570 manager.dispatch.init(self, args, kwargs)
    572 try:
    573     manager.original_init(*mixed[1:], **kwargs)

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/event/attr.py:487, in _CompoundListener.__call__(self, *args, **kw)
    485     fn(*args, **kw)
    486 for fn in self.listeners:
--> 487     fn(*args, **kw)

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py:4308, in _event_on_init(state, args, kwargs)
   4306 instrumenting_mapper = state.manager.mapper
   4307 if instrumenting_mapper:
-> 4308     instrumenting_mapper._check_configure()
   4309     if instrumenting_mapper._set_polymorphic_identity:
   4310         instrumenting_mapper._set_polymorphic_identity(state)

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py:2374, in Mapper._check_configure(self)
   2366 @util.langhelpers.tag_method_for_warnings(
   2367     "This warning originated from the `configure_mappers()` process, "
   2368     "which was invoked automatically in response to a user-initiated "
   (...)
   2371 )
   2372 def _check_configure(self) -> None:
   2373     if self.registry._new_mappers:
-> 2374         _configure_registries({self.registry}, cascade=True)

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py:4116, in _configure_registries(registries, cascade)
   4110     Mapper.dispatch._for_class(Mapper).before_configured()  # type: ignore # noqa: E501
   4111     # initialize properties on all mappers
   4112     # note that _mapper_registry is unordered, which
   4113     # may randomly conceal/reveal issues related to
   4114     # the order of mapper compilation
-> 4116     _do_configure_registries(registries, cascade)
   4117 finally:
   4118     _already_compiling = False

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py:4158, in _do_configure_registries(registries, cascade)
   4156 if not mapper.configured:
   4157     try:
-> 4158         mapper._post_configure_properties()
   4159         mapper._expire_memoizations()
   4160         mapper.dispatch.mapper_configured(mapper, mapper.class_)

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py:2391, in Mapper._post_configure_properties(self)
   2388 self._log("initialize prop %s", key)
   2390 if prop.parent is self and not prop._configure_started:
-> 2391     prop.init()
   2393 if prop._configure_finished:
   2394     prop.post_instrument_class(self)

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py:544, in MapperProperty.init(self)
    537 """Called after all mappers are created to assemble
    538 relationships between mappers and perform other post-mapper-creation
    539 initialization steps.
    540 
    541 
    542 """
    543 self._configure_started = True
--> 544 self.do_init()
    545 self._configure_finished = True

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py:1634, in RelationshipProperty.do_init(self)
   1632 self._setup_entity()
   1633 self._setup_registry_dependencies()
-> 1634 self._setup_join_conditions()
   1635 self._check_cascade_settings(self._cascade)
   1636 self._post_init()

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py:1881, in RelationshipProperty._setup_join_conditions(self)
   1880 def _setup_join_conditions(self) -> None:
-> 1881     self._join_condition = jc = JoinCondition(
   1882         parent_persist_selectable=self.parent.persist_selectable,
   1883         child_persist_selectable=self.entity.persist_selectable,
   1884         parent_local_selectable=self.parent.local_table,
   1885         child_local_selectable=self.entity.local_table,
   1886         primaryjoin=self._init_args.primaryjoin.resolved,
   1887         secondary=self._init_args.secondary.resolved,
   1888         secondaryjoin=self._init_args.secondaryjoin.resolved,
   1889         parent_equivalents=self.parent._equivalent_columns,
   1890         child_equivalents=self.mapper._equivalent_columns,
   1891         consider_as_foreign_keys=self._user_defined_foreign_keys,
   1892         local_remote_pairs=self.local_remote_pairs,
   1893         remote_side=self.remote_side,
   1894         self_referential=self._is_self_referential,
   1895         prop=self,
   1896         support_sync=not self.viewonly,
   1897         can_be_synced_fn=self._columns_are_mapped,
   1898     )
   1899     self.primaryjoin = jc.primaryjoin
   1900     self.secondaryjoin = jc.secondaryjoin

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py:2308, in JoinCondition.__init__(self, parent_persist_selectable, child_persist_selectable, parent_local_selectable, child_local_selectable, primaryjoin, secondary, secondaryjoin, parent_equivalents, child_equivalents, consider_as_foreign_keys, local_remote_pairs, remote_side, self_referential, prop, support_sync, can_be_synced_fn)
   2305 self.support_sync = support_sync
   2306 self.can_be_synced_fn = can_be_synced_fn
-> 2308 self._determine_joins()
   2309 assert self.primaryjoin is not None
   2311 self._sanitize_joins()

File ~/miniconda3/envs/excel/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py:2465, in JoinCondition._determine_joins(self)
   2453     raise sa_exc.AmbiguousForeignKeysError(
   2454         "Could not determine join "
   2455         "condition between parent/child tables on "
   (...)
   2462         "parent and child tables." % (self.prop, self.secondary)
   2463     ) from afe
   2464 else:
-> 2465     raise sa_exc.AmbiguousForeignKeysError(
   2466         "Could not determine join "
   2467         "condition between parent/child tables on "
   2468         "relationship %s - there are multiple foreign key "
   2469         "paths linking the tables.  Specify the "
   2470         "'foreign_keys' argument, providing a list of those "
   2471         "columns which should be counted as containing a "
   2472         "foreign key reference to the parent table." % self.prop
   2473     ) from afe

AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Address.customers_using_this_adress - 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.

>Solution :

Unlike a composite primary key where you can add primary_key=True to columns you want, composite foreign keys do not work that way. What you have right now is two different foreign keys and not a single composite foreign key. You need to use ForeignKeyConstraint in the __table_args__ tuple like the following.

    __table_args__ = (
        ForeignKeyConstraint(
            [customer_id, customer_country], [Customer.id, Customer.country]
        ),
    )

Full code

from sqlalchemy import Column, ForeignKey, Integer, String, ForeignKeyConstraint
from sqlalchemy.orm import DeclarativeBase, mapped_column, relationship


class Base(DeclarativeBase):
    pass


class Customer(Base):
    __tablename__ = "customer"
    id = mapped_column(String, primary_key=True)
    country = mapped_column(String, primary_key=True)

    billing_address_id = mapped_column(Integer, ForeignKey("address.idx"))
    shipping_address_id = mapped_column(Integer, ForeignKey("address.idx"))

    billing_address = relationship("Address", foreign_keys=[billing_address_id])
    shipping_address = relationship("Address", foreign_keys=[shipping_address_id])


class Address(Base):
    __tablename__ = "address"
    idx = mapped_column(Integer, primary_key=True)
    address = mapped_column(String)
    customer_id = mapped_column(Integer)
    customer_country = mapped_column(Integer)
    customers_using_this_adress = relationship(
        "Customer", foreign_keys=[customer_id, customer_country]
    )
    __table_args__ = (
        ForeignKeyConstraint(
            [customer_id, customer_country], [Customer.id, Customer.country]
        ),
    )

c = Customer(id="A", country="B")
Add a comment

Leave a Reply

Keep Up to Date with the Most Important News

By pressing the Subscribe button, you confirm that you have read and are agreeing to our Privacy Policy and Terms of Use

Discover more from Dev solutions

Subscribe now to keep reading and get access to the full archive.

Continue reading