首页 > 解决方案 > SQLAlchemy 命令式映射无法使用另一个实体的外键添加实体

问题描述

例如,在尝试使用外键添加新实体时,我遇到了一些 SQLAlchemy 命令式映射问题。

如果我将映射类定义为:

@attr.s
class Foreign:
    id = attr.ib(type=int, validator=optional(instance_of(int)), default=None)
    name = attr.ib(type=str, validator=instance_of(str), default=None)


@attr.s
class Primary:
    id = attr.ib(type=int, validator=optional(instance_of(int)), default=None)
    foreign_id = attr.ib(type=int, validator=optional(instance_of(int)), default=None)
    foreign = attr.ib(type=Foreign, validator=optional(instance_of(Foreign)), default=None)

添加新实体时,例如:

session.add(Primary(foreign_id=foreign_id))
session.commit()

它失败了,因为它用 None 替换了 foreign_id,因为我猜它期望填充外部属性。

这些是我的表:

mapper_registry = registry()

primary = Table(
    "primary",
    metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("foreign_id", Integer, ForeignKey("foreign.id"), nullable=False),
)


foreign = Table(
    "foreign",
    metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("name", String(20), nullable=False),
)


def configure_mappers():
    mapper_registry.map_imperatively(Foreign, foreign)

def configure_mappers():
    mapper_registry.map_imperatively(
        Primary,
        primary,
        properties={
            "foreign": relationship(Foreign),
        },
    )

但是,如果我foreign从 Mapped Class 中删除该属性,它就可以完美地工作。

如果我的映射类具有到外键和外键的映射,我是否可以使用外键添加新实体?

提前致谢

标签: pythonsqlalchemyormforeign-keysrelationship

解决方案


来自attrs 文档

尝试以简洁易用的方式设计你的类——而不是基于你的数据库格式。数据库格式可以随时更改,并且您会遇到难以更改的糟糕类设计。将函数和类方法作为现实与最适合您使用的方法之间的过滤器。

如果您绝对需要从 Python 代码访问 id 和/或从外部 id 和对象实例化 Primary 对象,您可以检查类实例方法并编写两个类方法来实例化 Primary:

  • 一个用外来对象实例实例化Primary,
  • 另一个使用外部对象 id 作为外键来实例化 Primary。

替代设计

据我了解,ORM 的价值(尤其是在其命令式映射风格中)是能够表达您的代码,而无需处理诸如 id 和外键之类的数据库概念。

此外,能够实例化具有离散 id 的 Primary 或 Foreign 对象与将其用作主键这一事实相矛盾:在 Python 代码中没有强制执行具有唯一键的约束。

命令式映射的优势在于将域对象与其数据库实现完全解耦,因此您可以让 SQLAlchemy 处理幕后的 id。

模型.py

import attr
from attr.validators import instance_of
from attr.validators import optional


@attr.s
class Foreign:
    name = attr.ib(type=str, validator=instance_of(str), default=None)


@attr.s
class Primary:
    name = attr.ib(type=str, validator=instance_of(str), default=None)
    foreign = attr.ib(type=Foreign, validator=optional(instance_of(Foreign)), default=None)

orm.py

import model
from sqlalchemy.orm import registry
from sqlalchemy.orm import relationship
from sqlalchemy.schema import MetaData
from sqlalchemy import Column
from sqlalchemy import Integer
from sqlalchemy import ForeignKey
from sqlalchemy import String
from sqlalchemy import Table


metadata = MetaData()
mapper_registry = registry()

table_primary = Table(
    "primary",
    metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("foreign_id", Integer, ForeignKey("foreign.id"), nullable=False),
)


table_foreign = Table(
    "foreign",
    metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("name", String(20), nullable=False),
)


def configure_mappers():
    mapper_registry.map_imperatively(model.Foreign, table_foreign)

    mapper_registry.map_imperatively(
        model.Primary,
        table_primary,
        properties={
            "foreign": relationship(model.Foreign),
        },
    )

用法

import orm
import model
import sqlalchemy

engine = sqlalchemy.create_engine("sqlite:///my_db.db")

orm.metadata.create_all(engine)
orm.configure_mappers()
Session = sqlalchemy.orm.sessionmaker(bind=engine)
session = Session()


foreign_1 = model.Foreign(name="first foreign")
foreign_2 = model.Foreign(name="second foreign")
foreign_3 = model.Foreign(name="third foreign")
primary_a = model.Primary(name="first primary", foreign=foreign_3)
primary_b = model.Primary(name="second primary", foreign=foreign_2)
session.add_all([foreign_1, primary_a, primary_b])
session.commit()

推荐阅读