SQLAlchemy ORM не работает с составными внешними ключами - PullRequest
1 голос
/ 28 апреля 2020

Я пытаюсь построить пример с несколькими родственными моделями, как показано ниже. У нас есть модель B с отношением 1: n с моделью C; тогда мы имеем модель A с отношением: 1 с B и отношением: 1 с C. (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)

, но ничего не изменилось. Я что-то ошибаюсь или это ошибка? (по крайней мере, сообщение об ошибке неверно ...)

1 Ответ

1 голос
/ 30 апреля 2020

Проблема здесь в том, что вы предоставляете __tablename__, поэтому вы должны объявить ForeignKeyConstraint в __table_args__, а не в теле класса.

Другими словами, следующий код не будет применять ограничение внешнего ключа к дочерней таблице ...

from sqlalchemy import (
    create_engine,
    Column,
    Integer,
    text,
    ForeignKeyConstraint,
    String,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

connection_uri = (
    r"mssql+pyodbc://@.\SQLEXPRESS/myDb?driver=ODBC+Driver+17+for+SQL+Server"
)
engine = create_engine(connection_uri, 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")

    def __repr__(self):
        return f"<Parent(id1={self.id1}, id2={self.id2}), parent_name='{self.parent_name}'>"


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

    def __repr__(self):
        return f"<Child(id={self.id}, child_name={self.child_name})>"


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

... но это will ...

from sqlalchemy import (
    create_engine,
    Column,
    Integer,
    text,
    ForeignKeyConstraint,
    String,
)
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

connection_uri = (
    r"mssql+pyodbc://@.\SQLEXPRESS/myDb?driver=ODBC+Driver+17+for+SQL+Server"
)
# connection_uri = "sqlite:///:memory:"
engine = create_engine(connection_uri, 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")

    def __repr__(self):
        return f"<Parent(id1={self.id1}, id2={self.id2}), parent_name='{self.parent_name}'>"


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

    def __repr__(self):
        return f"<Child(id={self.id}, child_name={self.child_name})>"


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/13/orm/extensions/declarative/table_config.html#table -конфигурация

...