ORM 映射类概述

ORM 类映射配置概述。

对于刚接触 SQLAlchemy ORM 或 Python 的读者,建议浏览 ORM 快速入门,最好是完成 SQLAlchemy 统一教程,其中在 使用 ORM 声明式形式定义表元数据 中首次介绍了 ORM 配置。

ORM 映射样式

SQLAlchemy 提供两种不同的映射器配置样式,然后针对它们的设置方式提供进一步的子选项。映射器样式的变化是为了满足各种开发者的偏好,包括用户定义类与映射到关系模式表和列的抽象程度,正在使用的类层次结构类型,包括是否存在自定义元类方案,最后,如果存在其他类检测方法,例如是否同时使用 Python dataclasses

在现代 SQLAlchemy 中,这些样式之间的差异大多是表面的;当使用特定 SQLAlchemy 配置样式来表达映射类的意图时,映射类的内部映射过程在很大程度上对每种样式都是相同的,最终结果始终是一个用户定义的类,该类具有针对可选择单元(通常由 Table 对象表示)配置的 Mapper,并且该类本身已 检测 以包括与关系操作相关的行为,这些行为既存在于类级别,也存在于该类的实例上。由于该过程在所有情况下基本相同,因此来自不同样式的映射类始终可以完全互操作。协议 MappedClassProtocol 可用于在使用类型检查器(如 mypy)时指示映射类。

原始映射 API 通常被称为“经典”样式,而更自动化的映射样式被称为“声明式”样式。SQLAlchemy 现在将这两种映射样式称为命令式映射声明式映射

无论使用哪种映射样式,从 SQLAlchemy 1.4 开始的所有 ORM 映射都源于一个名为 registry 的单一对象,该对象是映射类的注册表。使用此注册表,可以将一组映射器配置作为一组进行最终确定,特定注册表中的类可以在配置过程中按名称相互引用。

版本 1.4 中的变更: 声明式映射和经典映射现在分别称为“声明式”和“命令式”映射,并在内部统一,都源于表示相关映射集合的 registry 构造。

声明式映射

声明式映射是现代 SQLAlchemy 中构建映射的典型方式。最常见的模式是首先使用 DeclarativeBase 超类构建一个基类。生成的基类(在 SQLAlchemy 的文档中通常称为 Base,但可以具有任何所需名称)在子类化时会将声明式映射过程应用于从其派生的所有子类,相对于默认情况下特定于新基类的 registry。以下示例说明了声明式基类的使用,该基类随后用于声明式表映射

from sqlalchemy import Integer, String, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


# declarative base class
class Base(DeclarativeBase):
    pass


# an example mapping using the base
class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    fullname: Mapped[str] = mapped_column(String(30))
    nickname: Mapped[Optional[str]]

上面,DeclarativeBase 类用于生成一个新的基类(在 SQLAlchemy 的文档中,它通常被称为 Base,但可以具有任何所需名称),要映射的新类可以从该基类继承,如上所示,一个新的映射类 User 被构建。

版本 2.0 中的变更: DeclarativeBase 超类取代了 declarative_base() 函数和 registry.generate_base() 方法;超类方法与 PEP 484 工具集成,无需使用插件。有关迁移说明,请参阅 ORM 声明式模型

基类引用一个 registry 对象,该对象维护相关映射类的集合,以及一个 MetaData 对象,该对象保留一个 Table 对象集合,这些对象被映射到类。

主要声明式映射样式将在以下部分详细介绍

在声明式映射类的范围内,还存在两种关于如何声明 Table 元数据的方法。它们包括

  • 带有 mapped_column() 的声明式表 - 表列使用 mapped_column() 指令在映射类中内联声明(或者以传统形式,直接使用 Column 对象)。mapped_column() 指令还可以选择与类型注释一起使用,使用 Mapped 类,该类可以提供有关映射列的一些详细信息。列指令与 __tablename__ 和可选的 __table_args__ 类级指令相结合,将允许声明式映射过程构建一个 Table 对象以进行映射。

  • 声明式与命令式表 (又称混合声明式) - 而不是分别指定表名和属性,一个显式构建的 Table 对象与一个声明式映射的类关联。这种映射风格是“声明式”和“命令式”映射的混合,适用于映射类到 反射的 Table 对象,以及映射类到现有的核心构造(如连接和子查询)。

声明式映射的文档在 使用声明式映射类 继续。

命令式映射

命令式经典 映射是指使用 registry.map_imperatively() 方法配置映射类,其中目标类不包含任何声明式类属性。

提示

命令式映射形式是一种使用较少的映射形式,源自 2006 年 SQLAlchemy 的最初版本。它本质上是一种绕过声明式系统以提供更“精简”映射系统的方式,并且不提供现代功能,例如 PEP 484 支持。因此,大多数文档示例使用声明式形式,建议新用户从 声明式表 配置开始。

在版本 2.0 中变更: The registry.map_imperatively() 方法现在用于创建经典映射。 sqlalchemy.orm.mapper() 独立函数实际上已被移除。

在“经典”形式中,表元数据使用 Table 结构单独创建,然后通过 registry.map_imperatively() 方法与 User 类关联,在建立一个 registry 实例后。通常,所有彼此相关的映射类的 registry 只有一个实例

from sqlalchemy import Table, Column, Integer, String, ForeignKey
from sqlalchemy.orm import registry

mapper_registry = registry()

user_table = Table(
    "user",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
    Column("fullname", String(50)),
    Column("nickname", String(12)),
)


class User:
    pass


mapper_registry.map_imperatively(User, user_table)

有关映射属性(如与其他类的关系)的信息通过 properties 字典提供。以下示例说明了第二个 Table 对象,映射到一个名为 Address 的类,然后通过 relationship()User 链接

address = Table(
    "address",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("user_id", Integer, ForeignKey("user.id")),
    Column("email_address", String(50)),
)

mapper_registry.map_imperatively(
    User,
    user,
    properties={
        "addresses": relationship(Address, backref="user", order_by=address.c.id)
    },
)

mapper_registry.map_imperatively(Address, address)

请注意,使用命令式方法映射的类与使用声明式方法映射的类是完全可互换的。这两个系统最终创建相同的配置,包括一个 Table、用户定义的类,以及通过一个 Mapper 对象链接在一起。当我们谈论“Mapper 的行为”时,这包括使用声明式系统的情况 - 它仍然被使用,只是在幕后。

映射类基本组件

对于所有映射形式,类的映射可以通过传递最终成为 Mapper 对象部分的构造参数以多种方式配置。传递给 Mapper 的参数源自给定的映射形式,包括传递给 registry.map_imperatively() 的参数(用于命令式映射),或使用声明式系统时,从要映射的表列、SQL 表达式和关系以及 __mapper_args__ 等属性的组合。

The Mapper 类寻找的配置信息主要分为四类

要映射的类

这是我们在应用程序中构建的类。通常,对该类的结构没有限制。 [1] 当映射 Python 类时,该类只能有一个 Mapper 对象。 [2]

使用 声明式 映射风格时,要映射的类要么是声明式基类的子类,要么由装饰器或函数(如 registry.mapped())处理。

使用 命令式 风格时,该类直接作为 map_imperatively.class_ 参数传递。

表或其他 from 子句对象

在绝大多数常见情况下,这是一个 Table 的实例。对于更高级的用例,它也可以指任何类型的 FromClause 对象,最常见的替代对象是 SubqueryJoin 对象。

使用 声明式 映射风格时,目标表要么由声明式系统根据 __tablename__ 属性和提供的 Column 对象生成,要么通过 __table__ 属性建立。这两种配置风格在 声明式表与 mapped_column()声明式与命令式表 (又称混合声明式) 中介绍。

使用 命令式 风格时,目标表作为 map_imperatively.local_table 参数按位置传递。

与映射类“每个类一个映射器”的要求相反,作为映射目标的 Table 或其他 FromClause 对象可以与任意数量的映射关联。 Mapper 直接对用户定义的类应用修改,但不会以任何方式修改给定的 Table 或其他 FromClause 对象。

属性字典

这是一个包含与映射类关联的所有属性的字典。默认情况下,Mapper 会根据给定的 Table 生成此字典的条目,条目形式为 ColumnProperty 对象,每个对象都引用映射表的单个 Column。属性字典还将包含所有其他类型的 MapperProperty 对象,这些对象需要配置,最常见的是由 relationship() 构造生成的实例。

当使用 声明式 映射风格进行映射时,属性字典由声明式系统通过扫描要映射的类以查找适当的属性来生成。有关此过程的说明,请参见 使用声明式定义映射属性 部分。

当使用 命令式 映射风格进行映射时,属性字典将直接作为 properties 参数传递给 registry.map_imperatively(),该方法会将其传递给 Mapper.properties 参数。

其他映射器配置参数

当使用 声明式 映射风格进行映射时,可以通过 __mapper_args__ 类属性配置其他映射器配置参数。使用示例可在 使用声明式配置映射器选项 中找到。

当使用 命令式 映射风格进行映射时,关键字参数将传递给 registry.map_imperatively() 方法,该方法会将其传递给 Mapper 类。

接受的所有参数的完整范围在 Mapper 中有记录。

映射类行为

在使用 registry 对象的所有映射风格中,以下行为是通用的

默认构造函数

registry 为所有没有显式定义自己的 __init__ 方法的映射类应用默认构造函数,即 __init__ 方法。此方法的行为是,它提供了一个方便的关键字构造函数,该构造函数将接受所有命名的属性作为可选关键字参数。例如:

from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import Mapped
from sqlalchemy.orm import mapped_column


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(primary_key=True)
    name: Mapped[str]
    fullname: Mapped[str]

上面类型为 User 的对象将具有一个构造函数,该构造函数允许创建 User 对象,如下所示:

u1 = User(name="some name", fullname="some fullname")

提示

声明式数据类映射 功能提供了一种生成默认 __init__() 方法的替代方法,即使用 Python 数据类,并允许使用高度可配置的构造函数形式。

警告

类的 __init__() 方法仅在对象在 Python 代码中构建时调用,而 **不会在对象从数据库加载或刷新时调用**。有关如何在加载对象时调用特殊逻辑的信息,请参见下一节 跨加载维护非映射状态

包含显式 __init__() 方法的类将保留该方法,并且不会应用任何默认构造函数。

要更改使用的默认构造函数,用户定义的 Python 可调用对象可以提供给 registry.constructor 参数,该参数将用作默认构造函数。

构造函数也适用于命令式映射

from sqlalchemy.orm import registry

mapper_registry = registry()

user_table = Table(
    "user",
    mapper_registry.metadata,
    Column("id", Integer, primary_key=True),
    Column("name", String(50)),
)


class User:
    pass


mapper_registry.map_imperatively(User, user_table)

上面在 命令式映射 中描述的命令式映射的类也具有与 registry 关联的默认构造函数。

版本 1.4 中的新功能: 经典映射现在支持通过 registry.map_imperatively() 方法映射时,使用标准的配置级构造函数。

跨加载维护非映射状态

映射类的 __init__() 方法在对象直接在 Python 代码中构造时调用

u1 = User(name="some name", fullname="some fullname")

但是,当使用 ORM Session 加载对象时,**不会**调用 __init__() 方法

u1 = session.scalars(select(User).where(User.name == "some name")).first()

原因是,当从数据库加载时,用于构建对象(在上面的示例中为 User)的操作更类似于 **反序列化**,例如反序列化,而不是初始构建。对象的大部分重要状态不是在第一次被组装,而是从数据库行中重新加载。

因此,为了在对象中维护不属于存储到数据库中的数据的状态,使此状态在对象加载和构建时都存在,下面详细介绍了两种一般方法。

  1. 使用 Python 描述符,例如 @property,而不是状态,根据需要动态计算属性。

    对于简单的属性,这是最简单的方法,也是最不容易出错的方法。例如,如果一个具有 Point.xPoint.y 属性的 Point 对象想要一个具有这些属性之和的属性,那么:

    class Point(Base):
        __tablename__ = "point"
        id: Mapped[int] = mapped_column(primary_key=True)
        x: Mapped[int]
        y: Mapped[int]
    
        @property
        def x_plus_y(self):
            return self.x + self.y

    使用动态描述符的一个优点是每次都会计算值,这意味着它会在底层属性(在本例中为 xy)可能发生变化时维护正确的值。

    上面模式的其他形式包括 Python 标准库 cached_property 装饰器(它被缓存,并且不会每次都重新计算),以及 SQLAlchemy 的 hybrid_property 装饰器,它允许用于 SQL 查询的属性。

  2. 使用 InstanceEvents.load() 在加载时建立状态,并可选地使用补充方法 InstanceEvents.refresh()InstanceEvents.refresh_flush()

    这些是事件钩子,每当对象从数据库加载或在过期后刷新时调用。通常只需要 InstanceEvents.load(),因为非映射本地对象状态不受过期操作的影响。要修改上面的 Point 示例,可以这样做:

    from sqlalchemy import event
    
    
    class Point(Base):
        __tablename__ = "point"
        id: Mapped[int] = mapped_column(primary_key=True)
        x: Mapped[int]
        y: Mapped[int]
    
        def __init__(self, x, y, **kw):
            super().__init__(x=x, y=y, **kw)
            self.x_plus_y = x + y
    
    
    @event.listens_for(Point, "load")
    def receive_load(target, context):
        target.x_plus_y = target.x + target.y

    如果也使用刷新事件,则可以根据需要将事件钩子叠加在一个可调用对象之上,如下所示:

    @event.listens_for(Point, "load")
    @event.listens_for(Point, "refresh")
    @event.listens_for(Point, "refresh_flush")
    def receive_load(target, context, attrs=None):
        target.x_plus_y = target.x + target.y

    在上面,attrs 属性将存在于 refreshrefresh_flush 事件中,并指示正在刷新的属性名称列表。

映射类、实例和映射器的运行时自省

使用 registry 映射的类还将具有一些所有映射共有的属性

  • __mapper__ 属性将引用与该类关联的 Mapper

    mapper = User.__mapper__

    当对映射类使用 inspect() 函数时,也会返回此 Mapper

    from sqlalchemy import inspect
    
    mapper = inspect(User)
  • __table__ 属性将引用该类映射到的 Table,或者更一般地引用 FromClause 对象

    table = User.__table__

    FromClause 也是使用 Mapper.local_table 属性的 Mapper 时返回的结果。

    table = inspect(User).local_table

    对于单表继承映射,其中类是子类,没有自己的表,则 Mapper.local_table 属性以及 .__table__ 属性将为 None。要检索在对该类的查询期间实际选中的“可选择项”,可以通过 Mapper.selectable 属性获得。

    table = inspect(User).selectable

Mapper 对象的检查

如上一节所示,Mapper 对象可从任何映射类获得,无论使用哪种方法,都使用 运行时检查 API 系统。使用 inspect() 函数,可以从映射类中获取 Mapper

>>> from sqlalchemy import inspect
>>> insp = inspect(User)

包括 Mapper.columns 在内的详细信息可用。

>>> insp.columns
<sqlalchemy.util._collections.OrderedProperties object at 0x102f407f8>

这是一个命名空间,可以以列表格式或通过单个名称查看。

>>> list(insp.columns)
[Column('id', Integer(), table=<user>, primary_key=True, nullable=False), Column('name', String(length=50), table=<user>), Column('fullname', String(length=50), table=<user>), Column('nickname', String(length=50), table=<user>)]
>>> insp.columns.name
Column('name', String(length=50), table=<user>)

其他命名空间包括 Mapper.all_orm_descriptors,其中包括所有映射属性以及混合属性、关联代理。

>>> insp.all_orm_descriptors
<sqlalchemy.util._collections.ImmutableProperties object at 0x1040e2c68>
>>> insp.all_orm_descriptors.keys()
['fullname', 'nickname', 'name', 'id']

以及 Mapper.column_attrs

>>> list(insp.column_attrs)
[<ColumnProperty at 0x10403fde0; id>, <ColumnProperty at 0x10403fce8; name>, <ColumnProperty at 0x1040e9050; fullname>, <ColumnProperty at 0x1040e9148; nickname>]
>>> insp.column_attrs.name
<ColumnProperty at 0x10403fce8; name>
>>> insp.column_attrs.name.expression
Column('name', String(length=50), table=<user>)

另请参阅

Mapper

映射实例的检查

inspect() 函数还提供有关映射类实例的信息。当应用于映射类的实例而不是类本身时,返回的对象称为 InstanceState,它将提供链接到 not only the Mapper 类使用的,还提供一个详细的界面,提供有关实例中各个属性状态的信息,包括它们的当前值以及这与它们的数据库加载值之间的关系。

给定一个从数据库加载的 User 类的实例

>>> u1 = session.scalars(select(User)).first()

inspect() 函数将返回给我们一个 InstanceState 对象

>>> insp = inspect(u1)
>>> insp
<sqlalchemy.orm.state.InstanceState object at 0x7f07e5fec2e0>

使用此对象,我们可以看到诸如 Mapper 之类的元素

>>> insp.mapper
<Mapper at 0x7f07e614ef50; User>

对象 附加 到的 Session(如果有)

>>> insp.session
<sqlalchemy.orm.session.Session object at 0x7f07e614f160>

有关对象当前 持久状态 的信息

>>> insp.persistent
True
>>> insp.pending
False

属性状态信息,例如尚未加载或 延迟加载 的属性(假设 addresses 指的是映射类上到相关类的 relationship()

>>> insp.unloaded
{'addresses'}

有关属性当前在 Python 中的状态的信息,例如自上次刷新后未修改的属性

>>> insp.unmodified
{'nickname', 'name', 'fullname', 'id'}

以及自上次刷新后对属性进行修改的特定历史记录

>>> insp.attrs.nickname.value
'nickname'
>>> u1.nickname = "new nickname"
>>> insp.attrs.nickname.history
History(added=['new nickname'], unchanged=(), deleted=['nickname'])