SQLAlchemy ORM 不能使用复合外键

Posted

技术标签:

【中文标题】SQLAlchemy ORM 不能使用复合外键【英文标题】:SQLAlchemy ORM not working with composite foreign keys 【发布时间】:2020-04-28 10:35:32 【问题描述】:

我正在尝试使用几个相关模型构建一个示例,如下所示。 我们有一个模型 B 与模型 C 的关系为 1:n;那么我们有一个模型 A,它与 B 的关系为 n:1,与 C 的关系为 n:1。(C 有一个 2 列的主键)

我试过这段代码:

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))

只是为了获得例外:

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.

但是,嘿,我已经通过了那个论点。 我为该参数尝试了各种版本,指定 A 列,通过名称指定 C 列,通过直接引用指定 C 列,似乎没有任何影响此错误。 我还尝试使用单个复合外键,如下所示:

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)

但没有任何改变。 我弄错了什么还是一个错误? (至少,错误信息不正确……)

【问题讨论】:

在我看来,如果你让 A 成为 C 的孩子,那么你试图直接让 A 成为 B 的孩子是多余的,因为 C 是 B 的孩子。在 A 对象中有 related_b没有必要,因为related_c 还将确定与该 A 相关的 B。 (顺便说一句,更有意义的对象名称可能会更好地传达您的意图。) 关系 A -> B 和 A -> C 是独立的。每个 A 对象都有一个 B“父级”和一个 C“父级”(如果您愿意,可以称它们为母亲和父亲)。反过来,B 对象与几个 C 对象相关(其中一个可能与 A 相关,也可能不相关)。 (他们可能是……母亲的朋友?) 换句话说,A.other_rel.related_b 可能等于也可能不等于 A.related_b 我在 SQLAlchemy 中没有看到任何错误。 This code 对我来说很好。 它有一些不同。如果我理解正确,在您的代码中,每个关系都是 1:1,而在我的代码中有 n:1 关系。此外,在我的代码中,C 有一个 2 列主键,我不知道这是否是问题的一部分,但可能是。 【参考方案1】:

这里的问题是您必须在__table_args__ 中声明ForeignKeyConstraint,而不是在类的主体中。

换句话说,以下代码不会将外键约束应用于子表...

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, echo=True,)

with engine.connect() as conn:
    for tb_name in ["tbl_child", "tbl_parent"]:
        conn.execute(text(f"DROP TABLE IF EXISTS [tb_name]"))

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)
    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.create_all(engine)

""" console output:
2020-04-30 06:57:13,899 INFO sqlalchemy.engine.Engine 
CREATE TABLE tbl_parent (
    id1 INTEGER NOT NULL, 
    id2 INTEGER NOT NULL, 
    parent_name VARCHAR(50), 
    PRIMARY KEY (id1, id2)
)


2020-04-30 06:57:13,899 INFO sqlalchemy.engine.Engine ()
2020-04-30 06:57:13,900 INFO sqlalchemy.engine.Engine COMMIT
2020-04-30 06:57:13,901 INFO sqlalchemy.engine.Engine 
CREATE TABLE tbl_child (
    id INTEGER NOT NULL, 
    child_name VARCHAR(50), 
    parent_id1 INTEGER, 
    parent_id2 INTEGER, 
    PRIMARY KEY (id)
)


2020-04-30 06:57:13,901 INFO sqlalchemy.engine.Engine ()
2020-04-30 06:57:13,901 INFO sqlalchemy.engine.Engine COMMIT
"""

...但是这 ...

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, echo=True,)

with engine.connect() as conn:
    for tb_name in ["tbl_child", "tbl_parent"]:
        conn.execute(text(f"DROP TABLE IF EXISTS [tb_name]"))

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"
    __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.create_all(engine)

""" console output:
CREATE TABLE tbl_parent (
    id1 INTEGER NOT NULL, 
    id2 INTEGER NOT NULL, 
    parent_name VARCHAR(50) NULL, 
    PRIMARY KEY (id1, id2)
)


2020-04-30 07:52:43,771 INFO sqlalchemy.engine.Engine ()
2020-04-30 07:52:43,776 INFO sqlalchemy.engine.Engine COMMIT
2020-04-30 07:52:43,778 INFO sqlalchemy.engine.Engine 
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)
)


2020-04-30 07:52:43,778 INFO sqlalchemy.engine.Engine ()
2020-04-30 07:52:43,802 INFO sqlalchemy.engine.Engine COMMIT
"""

参考:

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

【讨论】:

非常感谢,已解决。不过,我更希望 SQLA 提供更清晰的错误消息。确实,我通过了那个论点;那是无效的,但它只是忽略了它。 “这里的问题是你提供了一个__tablename__,所以你必须在__table_args__中声明ForeignKeyConstraint,而不是在类的主体中”似乎有点误导,因为一个不跟随其他人。 @IljaEverilä - 感谢您的建议。

以上是关于SQLAlchemy ORM 不能使用复合外键的主要内容,如果未能解决你的问题,请参考以下文章

使用sqlalchemy的ORM创建外键关联时报错

sqlalchemy中复合外键的多个关系

SQLAlchemy:如何在删除时禁用 ORM 级外键处理?

西游之路——python全栈——ORM之SQLAlchemy外键与relationship的关系

Doctrine ORM:使用由外键组成的复合主键持久化集合

Python3-sqlalchemy-orm 联表查询-无外键关系