ORM 配置

如何映射没有主键的表?

为了映射到特定表,SQLAlchemy ORM 需要至少有一个列被标记为主键列;多列(即复合)主键当然也完全可行。这些列不需要实际在数据库中被识别为主键列,尽管它们是主键列是一个好主意。唯一的要求是这些列的行为必须与主键相同,例如作为行的唯一且不可为空的标识符。

大多数 ORM 要求对象定义某种类型的主键,因为内存中的对象必须对应于数据库表中唯一可识别的行;至少,这允许对象可以被定位用于 UPDATE 和 DELETE 语句,这些语句将只影响该对象的特定行,而不会影响其他任何行。但是,主键的重要性远远超出了此范围。在 SQLAlchemy 中,所有 ORM 映射的对象始终在 Session 中与其特定数据库行唯一关联,使用称为标识映射 的模式,此模式是 SQLAlchemy 使用的工作单元系统的核心,也是大多数(以及非典型)ORM 使用模式的关键。

注意

重要的是要注意,我们只讨论 SQLAlchemy ORM;基于 Core 并且只处理 Table 对象、select() 结构等应用程序不需要在任何方式上存在于表上或与表相关联的主键(尽管再次,在 SQL 中,所有表都应该真正具有某种类型的 主键,否则您将需要实际更新或删除特定行)。

在几乎所有情况下,表都具有所谓的候选键,它是一列或一系列唯一标识行的列。如果表真的没有候选键,并且具有完全重复的行,则该表不符合第一范式,无法映射。否则,构成最佳候选键的任何列都可以直接应用到映射器

class SomeClass(Base):
    __table__ = some_table_with_no_pk
    __mapper_args__ = {
        "primary_key": [some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar]
    }

更好的方法是,当使用完全声明的表元数据时,在这些列上使用 primary_key=True 标志

class SomeClass(Base):
    __tablename__ = "some_table_with_no_pk"

    uid = Column(Integer, primary_key=True)
    bar = Column(String, primary_key=True)

关系数据库中的所有表都应该具有主键。即使是多对多关联表 - 主键也将是两个关联列的组合

CREATE TABLE my_association (
  user_id INTEGER REFERENCES user(id),
  account_id INTEGER REFERENCES account(id),
  PRIMARY KEY (user_id, account_id)
)

如何配置一个 Python 保留字或类似的列?

基于列的属性可以在映射中指定任何想要的名称。请参见显式命名声明性映射列.

如何获取给定映射类的所有列、关系、映射属性等的列表?

所有这些信息都可以在 Mapper 对象中找到。

要获得特定映射类的 Mapper,请在其上调用 inspect() 函数

from sqlalchemy import inspect

mapper = inspect(MyClass)

从那里开始,所有关于该类的信息都可以通过以下属性访问

我收到关于“隐式组合属性 Y 下的列 X”的警告或错误

这种情况是指,当映射包含两列,由于它们的名称,这两列在相同的属性名称下被映射,但没有迹象表明这是故意的。映射类需要为每个存储独立值的属性指定显式名称;当两个列具有相同的名称且没有区分时,它们将位于相同的属性下,其效果是,来自一个列的值被复制到另一个列中,具体取决于哪个列首先被分配给该属性。

这种行为通常是理想的,并且在两列通过继承映射中的外键关系链接在一起的情况下,无需警告即可允许。当警告或异常发生时,可以通过将列分配给不同名称的属性来解决此问题,或者如果希望将它们组合在一起,可以使用 column_property() 来明确地实现这一点。

假设有以下示例

from sqlalchemy import Integer, Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

从 SQLAlchemy 版本 0.9.5 开始,上述情况会被检测到,并会警告说 id 列在 AB 中被组合到同一个名称的属性 id 下,这上面是一个严重的问题,因为它意味着 B 对象的主键将始终反映其 A 的主键。

解决此问题的映射如下

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    b_id = Column("id", Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

假设我们确实希望 A.idB.id 互相镜像,尽管事实上 B.a_idA.id 所关联的地方。我们可以使用 column_property() 将它们组合在一起

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    # probably not what you want, but this is a demonstration
    id = column_property(Column(Integer, primary_key=True), A.id)
    a_id = Column(Integer, ForeignKey("a.id"))

我正在使用声明式映射,并使用 and_()or_() 设置 primaryjoin/secondaryjoin,但我收到了有关外键的错误消息。

你是在这样做吗?

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
    )

这是一个 and_(),它包含两个字符串表达式,SQLAlchemy 无法对它们进行任何映射。声明式映射允许 relationship() 参数以字符串形式指定,这些字符串使用 eval() 转换为表达式对象。但这不会发生在 and_() 表达式内部 - 这是一个声明式映射专门应用于传递给 primaryjoin 或其他参数的字符串的全部内容的操作

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)"
    )

或者,如果所需的这些对象已经可用,则跳过字符串

class MyClass(Base):
    # ....

    foo = relationship(
        Dest, primaryjoin=and_(MyClass.id == Dest.foo_id, MyClass.foo == Dest.bar)
    )

同样的想法也适用于所有其他参数,例如 foreign_keys

# wrong !
foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"])

# correct !
foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]")

# also correct !
foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id])


# if you're using columns from the class that you're inside of, just use the column objects !
class MyClass(Base):
    foo_id = Column(...)
    bar_id = Column(...)
    # ...

    foo = relationship(Dest, foreign_keys=[foo_id, bar_id])

什么是 defaultdefault_factoryinsert_default,我应该使用哪个?

由于添加了 PEP-681 数据类转换,SQLAlchemy 的 API 中存在一些冲突,该转换对其命名约定非常严格。如果你使用的是 MappedAsDataclass,如 声明式数据类映射 中所示,PEP-681 将生效。如果你没有使用 MappedAsDataclass,则它不适用。

第一部分 - 不使用数据类的经典 SQLAlchemy

使用 MappedAsDataclass 时,这在 SQLAlchemy 中已经存在多年了,mapped_column()(和 Column)构造支持一个参数 mapped_column.default。这表示一个 Python 侧的默认值(与数据库模式定义的一部分的服务器端默认值相反),该默认值将在发出 INSERT 语句时生效。此默认值可以是任何静态 Python 值(如字符串), Python 可调用函数, SQLAlchemy SQL 构造。有关 mapped_column.default 的完整文档,请参阅 客户端调用的 SQL 表达式

当在使用 MappedAsDataclass 的 ORM 映射中使用 mapped_column.default 时,此默认值/可调用函数不会在你首次构造对象时显示在你的对象上。它只会在 SQLAlchemy 生成你的对象的 INSERT 语句时生效。

需要注意的一件非常重要的事情是,当使用 mapped_column()(和 Column)时,经典的 mapped_column.default 参数也可以使用一个新名称,名为 mapped_column.insert_default。如果你构建了一个 mapped_column() 并且你没有使用 MappedAsDataclass,则 mapped_column.defaultmapped_column.insert_default 参数是同义词

第二部分 - 在 MappedAsDataclass 中使用 Dataclasses 支持

当您正在使用 MappedAsDataclass 时,即在 声明式 Dataclass 映射 中使用的特定映射形式,mapped_column.default 关键字的含义会发生变化。我们认识到,这个名称改变其行为并非理想,但别无选择,因为 PEP-681 要求 mapped_column.default 承担这个含义。

当使用 dataclasses 时,mapped_column.default 参数必须按照 Python Dataclasses 中描述的方式使用 - 它指的是一个常数值,例如字符串或数字,并且在构造时立即应用于您的对象。目前它也应用于 mapped_column.default 参数的 Column,即使在对象中不存在时,它也会在 INSERT 语句中自动使用。如果您想在 dataclass 中使用一个可调用对象,它将在构造时应用于对象,那么您将使用 mapped_column.default_factory

要访问上面第一部分中描述的 mapped_column.default 的仅 INSERT 行为,您将使用 mapped_column.insert_default 参数。当使用 dataclasses 时,mapped_column.insert_default 仍然是 Core 级别的“默认”过程的直接途径,其中参数可以是静态值或可调用对象。

总结图表

构造

与 dataclasses 一起使用?

在没有 dataclasses 的情况下使用?

接受标量?

接受可调用对象?

立即填充对象?

mapped_column.default

只有在没有 dataclasses 的情况下

只有在 dataclasses 的情况下

mapped_column.insert_default

mapped_column.default_factory

只有在 dataclasses 的情况下