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 版本开始,检测到上述情况,并将警告 ABid 列正在相同的命名属性 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 表达式

当将 mapped_column.default 使用 MappedAsDataclass 的 ORM 映射一起使用时,此默认值/可调用对象 在您首次构造对象时不会显示在您的对象上。 它仅在 SQLAlchemy 为您的对象准备 INSERT 语句时才会生效。

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

第二部分 - 将数据类支持与 MappedAsDataclass 结合使用

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

当使用数据类时,mapped_column.default 参数必须按照 Python 数据类 中描述的方式使用 - 它指的是常量值(如字符串或数字),并且 在构造对象时立即应用于您的对象。 它目前也自动应用于 Columnmapped_column.default 参数,即使对象上不存在,它也会在 INSERT 语句中自动使用。 如果您想为您的数据类使用可调用对象,该对象将在构造对象时应用,您可以使用 mapped_column.default_factory

要访问上面第一部分中描述的 mapped_column.default 的仅 INSERT 行为,您应该改用 mapped_column.insert_default 参数。 当使用数据类时,mapped_column.insert_default 继续是 Core 级“default”过程的直接途径,其中该参数可以是静态值或可调用对象。

摘要图表

构造

与数据类一起使用?

不使用数据类时工作?

接受标量?

接受可调用对象?

立即填充对象?

mapped_column.default

仅当没有数据类时

仅当有数据类时

mapped_column.insert_default

mapped_column.default_factory

仅当有数据类时