Search code examples
pythonormsqlalchemyforeign-keysrelationship

SQLAlchemy ORM not working with composite foreign keys


I'm trying to build an example with several related models, like the following. We have a model B with a 1:n relation with a model C; then we have a model A with a n:1 relation with B and a n:1 relation with C. (C has a 2-columns primary key)

I tried this code:

class C(db.Model):
    __tablename__ = 'C'
    key1 = Column(Integer, primary_key=True)
    key2 = Column(Integer, primary_key=True)
    attr1 = Column(Date)
    attr2 = Column(Boolean)
    related_b = Column(Integer, ForeignKey('B.spam'))


class B(db.Model):
    __tablename__ = 'B'
    spam = Column(Integer, default=1, primary_key=True)
    eggs = Column(String, default='eggs')
    null = Column(String)
    n_relation = relationship(C, foreign_keys='C.related_b')


class A(db.Model):
    __tablename__ = 'A'
    foo = Column(String, default='foo', primary_key=True)
    bar = Column(String, default='bar', primary_key=True)
    baz = Column(String, default='baz')
    rel = relationship(B, foreign_keys='A.related_b')
    related_b = Column(Integer, ForeignKey('B.spam'))
    related_c1 = Column(Integer, ForeignKey('C.key1'))
    related_c2 = Column(Integer, ForeignKey('C.key2'))
    other_rel = relationship(C, foreign_keys=(related_c1, related_c2))

just to get the exception:

sqlalchemy.exc.AmbiguousForeignKeysError: Could not determine join condition between parent/child tables on relationship A.other_rel - 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.

but hey, I HAVE passed that argument. I tried various versions for that argument, specifying A columns, C columns via name, C columns via direct reference, nothing seems to affect this error. I also tried to use a single compound foreign key, like this:

class A(db.Model):
    __tablename__ = 'A'
    foo = Column(String, default='foo', primary_key=True)
    bar = Column(String, default='bar', primary_key=True)
    baz = Column(String, default='baz')
    rel = relationship(B, foreign_keys='A.related_b')
    related_b = Column(Integer, ForeignKey('B.spam'))
    related_c1 = Column(Integer, ForeignKey('C.key1'))
    related_c2 = Column(Integer, ForeignKey('C.key2'))
    compound = ForeignKeyConstraint(('related_c1', 'related_c2'), ('C.key1', 'C.key2'))
    other_rel = relationship(C, foreign_keys=compound)

but nothing changed. Am I mistaking something or is it a bug? (at least, the error message is not correct...)


Solution

  • The issue here is that you have to declare the ForeignKeyConstraint() in __table_args__, not in the body of the class.

    In other words, the following code will NOT apply the foreign key constraint to the child table …

    from sqlalchemy import create_engine, Column, Integer, text, ForeignKeyConstraint, String
    from sqlalchemy.orm import declarative_base, relationship
    
    connection_url = r"mssql+pyodbc://@.\SQLEXPRESS/myDb?driver=ODBC+Driver+17+for+SQL+Server"
    engine = create_engine(connection_url)
    Base = declarative_base()
    
    
    class Parent(Base):
        __tablename__ = "tbl_parent"
        id1 = Column(Integer, primary_key=True)
        id2 = Column(Integer, primary_key=True)
        parent_name = Column(String(50))
        children = relationship("Child", back_populates="parent")
    
    
    class Child(Base):
        __tablename__ = "tbl_child"
        id = Column(Integer, primary_key=True, autoincrement=False)
        child_name = Column(String(50))
        parent_id1 = Column(Integer)
        parent_id2 = Column(Integer)
        # this does not work
        ForeignKeyConstraint(
            ["parent_id1", "parent_id2"], ["tbl_parent.id1", "tbl_parent.id2"]
        )
        parent = relationship(
            "Parent",
            foreign_keys="[Child.parent_id1, Child.parent_id2]",
            back_populates="children",
        )
    
    
    Base.metadata.drop_all(engine)
    engine.echo = True
    Base.metadata.create_all(engine)
    
    """DDL emitted:
    CREATE TABLE tbl_parent (
        id1 INTEGER NOT NULL, 
        id2 INTEGER NOT NULL, 
        parent_name VARCHAR(50), 
        PRIMARY KEY (id1, id2)
    )
    CREATE TABLE tbl_child (
        id INTEGER NOT NULL, 
        child_name VARCHAR(50), 
        parent_id1 INTEGER, 
        parent_id2 INTEGER, 
        PRIMARY KEY (id)
    )
    """
    

    … but this will work …

    from sqlalchemy import create_engine, Column, Integer, text, ForeignKeyConstraint, String
    from sqlalchemy.orm import declarative_base, relationship
    
    connection_url = r"mssql+pyodbc://@.\SQLEXPRESS/myDb?driver=ODBC+Driver+17+for+SQL+Server"
    engine = create_engine(connection_url)
    Base = declarative_base()
    
    
    class Parent(Base):
        __tablename__ = "tbl_parent"
        id1 = Column(Integer, primary_key=True)
        id2 = Column(Integer, primary_key=True)
        parent_name = Column(String(50))
        children = relationship("Child", back_populates="parent")
    
    
    class Child(Base):
        __tablename__ = "tbl_child"
        # this works
        __table_args__ = (
            ForeignKeyConstraint(
                ["parent_id1", "parent_id2"], ["tbl_parent.id1", "tbl_parent.id2"]
            ),
        )
        id = Column(Integer, primary_key=True, autoincrement=False)
        child_name = Column(String(50))
        parent_id1 = Column(Integer)
        parent_id2 = Column(Integer)
    
        parent = relationship(
            "Parent",
            foreign_keys="[Child.parent_id1, Child.parent_id2]",
            back_populates="children",
        )
    
    
    Base.metadata.drop_all(engine)
    engine.echo = True
    Base.metadata.create_all(engine)
    
    """DDL emitted:
    CREATE TABLE tbl_parent (
        id1 INTEGER NOT NULL, 
        id2 INTEGER NOT NULL, 
        parent_name VARCHAR(50) NULL, 
        PRIMARY KEY (id1, id2)
    )
    CREATE TABLE tbl_child (
        id INTEGER NOT NULL, 
        child_name VARCHAR(50) NULL, 
        parent_id1 INTEGER NULL, 
        parent_id2 INTEGER NULL, 
        PRIMARY KEY (id), 
        FOREIGN KEY(parent_id1, parent_id2) REFERENCES tbl_parent (id1, id2)
    )
    """
    

    Reference:

    https://docs.sqlalchemy.org/en/14/orm/declarative_tables.html#orm-declarative-table-configuration