ORM 映射类概述

ORM 类映射配置概述。

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

ORM 映射风格

SQLAlchemy 具有两种不同的映射器配置风格,这些风格又具有进一步的子选项,用于设置它们。映射器风格的多样性是为了适应不同的开发者偏好,包括用户定义的类与如何将其映射到关系模式表和列的抽象程度,正在使用的类层次结构的类型,包括是否存在自定义元类方案,以及最终是否存在其他类检测方法,例如是否同时使用 Python dataclasses

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

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

无论使用何种映射风格,截至 SQLAlchemy 1.4,所有 ORM 映射都源自一个名为 registry 的单个对象,它是映射类的注册表。使用此注册表,可以将一组映射器配置作为一组最终确定,并且特定注册表中的类可以在配置过程中通过名称相互引用。

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

声明式映射

声明式映射是在现代 SQLAlchemy 中构建映射的典型方式。最常见的模式是首先使用 DeclarativeBase 超类构造基类。生成的基类在子类化时会将声明式映射过程应用于从它派生的所有子类,相对于默认情况下对新基类本地的特定 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 对象,以及将类映射到现有的 Core 构造(如连接和子查询)的技术。

有关声明式映射的文档,请继续阅读 使用声明式映射类

命令式映射

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

提示

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

在版本 2.0 中更改: 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__ 等属性。

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

提示

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

警告

仅当在 Python 代码中构造对象时才调用类的 __init__() 方法,而不是在从数据库加载或刷新对象时调用。有关如何在加载对象时调用特殊逻辑的入门知识,请参阅下一节 在加载时维护非映射状态

包含显式 __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() 方法映射时,经典映射现在支持标准的配置级构造函数。

跨加载维护非映射状态

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

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

然而,当使用 ORM Session 加载对象时,不会调用 __init__() 方法

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

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

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

  1. 使用像 @property 这样的 Python 描述符,而不是状态,来根据需要动态计算属性。

    对于简单属性,这是最简单且最不易出错的方法。 例如,如果一个具有 Point.xPoint.yPoint 对象想要一个包含这些属性之和的属性

    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__

    当使用 Mapper.local_table 属性时,也会返回此 FromClause,该属性属于 Mapper

    table = inspect(User).local_table

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

    table = inspect(User).selectable

Mapper 对象的自省

正如上一节所示,可以使用 运行时自省 API 系统从任何映射类中获取 Mapper 对象,而与方法无关。 使用 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,它将提供指向类使用的 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>

对象 attached 到的 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'])