I am trying to model a complex relationship using SQLModel with Postgresql database. In that, I am querying the User and Subscriber which, between them has a many-to-one relationship as well as an established F'Key. Similarly, User has a m:1 relationship with UserRole, and Subscriber also has m:1 relationships with SubscriberType and 1:m with Address.

Address in turn has other m:1 and 1:1 relationships with other classes and corresponding tables; but for the sake of brevity, let's skip them. Accordingly I skinned out my model to show here the core issue.

I think what it is not liking is that Subscriber has a FK to Address and Address has a FK to Subscriber, which may in turn create multiple FK relation paths between the two. If my interpretation of the error is right in relation to the model, then I do not know the best way to break / represent it. What is the best practice in such cases?

Biz situation is that a Subscriber, a Supplier, a Partner all have an address and may even multiple addresses. So, each has a 1:m to Address. An Address, in turn can belong to any of those entities and thus will have a FK to point to the right one. My representation may not be correct but do not know how else to do this.

Admittedly I have started to learn both Python and ORM (SQLModel); so my apologies in advance for naive mistakes and gratitude to help me understand my mistakes and guide me to the right solution.

I tried three options with the same results - thank God at least for that. It confirmed my understanding on modeling the relationships and /or creating the joined query is totally off.

What I saw in other posts and suggestions to similar issues calling for adding explicit foreign_keys[] in the relationship of the child class while omitting the relationship from the parent. I couldn't do this as it kept complaining about unexpected argument.

Many also suggested to use sa_relationship_kwargs=dict(lazy="selectin") in the child relationship; but candidly, I did not quite understand how this works or why this could solve the issue, to be able to properly use it.

Here's my code. All three options are annotated inline in the code below.

from decouple import config
from sqlalchemy import text
from sqlmodel import SQLModel, Field, Relationship, select, Session, join, and_
from typing import Optional, List

from connection import engine

dbschema = config('dbschema')  # application actors details
scschema = config('scschema')  # biz objects


class SubscriberType(SQLModel, table=True):
    __tablename__ = 'subscriber_types_test'
    __table_args__ = {'schema': dbschema}

    type: str = Field(default=None, primary_key=True)

    subscribers: List['Subscriber'] = Relationship(back_populates="subscribertype")


class UserRole(SQLModel, table=True):
    __tablename__ = 'user_roles_test'
    __table_args__ = {'schema': dbschema}

    role: str = Field(default=None, primary_key=True)

    users: List['User'] = Relationship(back_populates="userrole")


class Subscriber(SQLModel, table=True):
    __tablename__ = 'subscribers_test'
    __table_args__ = {'schema': dbschema}

    id: Optional[int] = Field(default=None, primary_key=True)
    name: str = Field(nullable=False, index=True)

    address_id: Optional[int] = Field(foreign_key=scschema + '.addresses_test.id', index=True)
    addresses: List['Address'] = Relationship(back_populates='subscriber')

    subscriber_type: str = Field(nullable=False, default='regular',
                                 foreign_key=dbschema + '.subscriber_types_test.type'
                                 )
    subscribertype: SubscriberType = Relationship(back_populates='subscribers')

    users: List['User'] = Relationship(back_populates='subscriber')


class User(SQLModel, table=True):
    __tablename__ = 'users_test'
    __table_args__ = {'schema': dbschema}

    id: Optional[int] = Field(default=None, primary_key=True)

    subscriber_id: int = Field(nullable=False, foreign_key=dbschema + '.subscribers_test.id', index=True)
    subscriber: Subscriber = Relationship(back_populates='users')

    email: str = Field(nullable=False, unique=True, index=True)
    username: str = Field(nullable=False)

    role: str = Field(nullable=False, default='user', foreign_key=dbschema + '.user_roles_test.role')
    userrole: UserRole = Relationship(back_populates='users')


class Address(SQLModel, table=True):
    __tablename__ = 'addresses_test'
    __table_args__ = {'schema': scschema}

    id: Optional[int] = Field(default=None, primary_key=True)
    lat: Optional[float]
    lon: Optional[float]

    subscriber_id: Optional[int] = Field(foreign_key=dbschema + '.subscribers_test.id', index=True)
    subscriber: Optional['Subscriber'] = Relationship(back_populates='addresses')

# Below was my first option without selective columns and criteria
# stmt = select(User, Subscriber).select_from(join(User, Subscriber))


# Tried this option with more detailed query and filters and this runs perfectly in PGAdmin portal
stmt = text('Select U.username as "username", \
                U.email as "email", \
                U.role as "user_role", \
                S.name as "subscribername" \
           From test.users_test U, test.subscribers_test S \
           Where U.email = "[email protected]" and U.subscriber_id = S.id'
            )

with (Session(engine) as session):
    SQLModel.metadata.create_all(session.bind)

    substype = SubscriberType(type="regular")
    role = UserRole(role="admin")
    addr = Address()
    subs = Subscriber(name="xyz corp")
    user = User(email="[email protected]", username="abc")
    subs.subscribertype = substype
    addr.subscriber = subs
    user.userrole = role

    session.add(substype)
    session.add(role)
    session.add(addr)
    session.add(subs)
    session.add(user)
    session.commit()

    # Below statement used with the first two options
    # data = session.exec(stmt)

    # This was my third option
    data = session.query(user.email, user.username, user.role, subs.name
                         ).where(and_(user.email == '[email protected]', user.subscriber_id == subs.id)
                                 ).first()

print(data)

I expect the result returned should be:

[("abc", "[email protected]", "regular", "xyz corp")]

But I am getting the following error (full Traceback):

 Traceback (most recent call last):
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2754, in _determine_joins
    self.primaryjoin = join_condition(
                       ^^^^^^^^^^^^^^^
  File "<string>", line 2, in join_condition
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1217, in _join_condition
    cls._joincond_trim_constraints(
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py", line 1338, in _joincond_trim_constraints
    raise exc.AmbiguousForeignKeysError(
sqlalchemy.exc.AmbiguousForeignKeysError: Can't determine join between 'subscribers_test' and 'addresses_test'; 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:

Traceback (most recent call last):
  File "/Users/tapas/PycharmProjects/ResiChainLab/multirelationtest/main.py", line 89, in <module>
    substype = SubscriberType(type="regular")
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<string>", line 4, in __init__
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/state.py", line 476, in _initialize_instance
    manager.dispatch.init(self, args, kwargs)
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/event/attr.py", line 334, in __call__
    fn(*args, **kw)
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 3716, in _event_on_init
    instrumenting_mapper._check_configure()
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 1941, in _check_configure
    _configure_registries({self.registry}, cascade=True)
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 3527, in _configure_registries
    _do_configure_registries(registries, cascade)
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 3566, in _do_configure_registries
    mapper._post_configure_properties()
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py", line 1958, in _post_configure_properties
    prop.init()
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py", line 231, in init
    self.do_init()
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2152, in do_init
    self._setup_join_conditions()
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2248, in _setup_join_conditions
    self._join_condition = jc = JoinCondition(
                                ^^^^^^^^^^^^^^
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2643, in __init__
    self._determine_joins()
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py", line 2806, in _determine_joins
    util.raise_(
  File "/Users/tapas/Anaconda/anaconda3/envs/multirelationtest/lib/python3.11/site-packages/sqlalchemy/util/compat.py", line 211, in raise_
    raise exception
sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship Subscriber.addresses - 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.

What am I missing to take note of? Where are the "multiple foreign key paths" coming from?

0

There are 0 best solutions below