ORM 映射类概述


ORM 类映射配置概述。


对于刚接触 SQLAlchemy ORM 和/或 Python 的读者, 建议浏览 ORM 快速入门,最好完成 SQLAlchemy Unified Tutorial,其中 ORM 配置首次在 使用 ORM 声明式表单定义表元数据


ORM 映射样式


SQLAlchemy 具有两种不同风格的 mapper 配置,然后具有有关如何设置它们的更多子选项。映射器样式的可变性是为了适应各种开发人员首选项,包括用户定义类的抽象程度(从如何映射到关系架构表和列)、正在使用的类层次结构类型(包括是否存在自定义元类方案),以及最后是否存在其他类插桩方法,例如是否同时使用 Python 数据类


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


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


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


在 1.4 版本发生变更: 声明性映射和经典映射现在称为“声明性”和“命令性”映射,并且在内部是统一的,所有这些都源自表示相关映射集合的注册表构造。


声明式映射


Declarative Mapping 是在现代 SQLAlchemy 中构造 Map 的典型方式。最常见的模式是首先使用 DeclarativeBase 超类构造一个基类。生成的 base 类在子类化时,将声明性 Map 过程应用于从它派生的所有子类,相对于默认情况下是新 base 本地的特定注册表。下面的示例说明了声明性基的用法,然后将其用于声明性表映射:

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 Declarative Models


基类引用维护相关映射类集合的注册表对象。以及 MetaData 对象,该对象保留类映射到的 Table 对象的集合。


以下部分将进一步详细介绍主要的 Declarative 映射样式:


在 Declarative 映射类的范围内,还有两种 Table 元数据的声明方式。这些包括:


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


命令式映射


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


提示


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


在 2.0 版更改: registry.map_imperatively() 方法现在用于创建经典映射。sqlalchemy.orm.mapper() Standalone 函数被有效地删除。


在 “classical” 形式中,表元数据是使用 Table 结构,然后在建立注册表实例后通过 registry.map_imperatively() 方法与 User 类关联。 通常,单个 注册表 shared 对于彼此相关的所有映射类:

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)


有关 Map 属性的信息(例如与其他类的关系)通过 properties 字典提供。下面的示例说明了第二个 对象,映射到一个名为 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)


请注意,使用 Imperative 方法映射的类是完全的 可与使用 Declarative 方法映射的 API 互换。两种系统 最终创建相同的配置,包括 ,用户定义的类,通过 Mapper 对象。当我们谈论“行为 Mapper“,这也包括在使用 Declarative 系统时 - 它仍在使用,只是在幕后。


映射类基本组件


对于所有映射形式,可以通过传递最终成为 Mapper 一部分的构造参数以多种方式配置类的映射 Object 的构造函数。 传送到 Mapper 源自给定的映射表单,包括传递给 registry.map_imperatively() 的 Imperative 的参数 mapping,或者在使用 Declarative 系统时,从组合 的表列、SQL 表达式和 关系与属性(如 __mapper_args__


配置信息有四类一般类,其中 Mapper 类查找:


要映射的类


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


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


当使用命令式样式进行映射时, 类直接作为 map_imperatively.class_ 参数。


table 或其他 from 子句对象


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


当使用声明式映射样式进行映射时,主题表要么由声明式系统基于 __tablename__ 属性和显示的 Column 对象生成,要么通过 __table__ 属性建立。 这些 两种配置样式在 带有 mapped_column() 的声明式表带有命令式表的声明式表(又名混合声明式)。


当使用命令式样式进行映射时, subject table 作为 map_imperatively.local_table 参数。


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


properties 字典


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


当使用声明式映射样式进行映射时,properties 字典由声明式系统通过扫描要映射的类来生成适当的属性。有关此过程的说明,请参阅使用 Declare Defining Mapped Properties 部分。


当使用命令式样式进行映射时, properties 字典直接作为 properties 参数传递给 registry.map_imperatively(),这会将其传递给 Mapper.properties 参数。


其他 mapper 配置参数


当使用声明式映射进行映射时 style,其他映射器配置参数通过 __mapper_args__ class 属性。使用示例可在 Mapper Configuration Options with Declare 中找到


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


Mapper 中记录了接受的所有参数。


映射类行为


在使用注册表对象的所有映射样式中,以下行为是常见的:


默认构造函数


注册表应用默认构造函数,即 __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 类型的对象将具有一个构造函数,该构造函数允许 要创建的用户对象为:

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


提示


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


警告


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


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


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


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

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)


上述类(如 命令式映射中所述)也将具有与注册表关联的默认构造函数。


在 1.4 版本加入: 经典映射现在通过 registry.map_imperatively() 映射时支持标准配置级构造函数 方法。


在 loads之间保持 non-mapped 状态


当直接在 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)更类似于反序列化,例如解封,而不是初始构造。对象的大部分重要状态不是第一次组装,而是从数据库行重新加载。


因此,要在不属于存储到数据库的数据的对象中保持状态,以便在加载和构建对象时存在此状态,下面详细介绍了两种常规方法。


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


    对于简单属性,这是最简单的方法,也是最不容易出错的。例如,如果具有 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


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

    @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 属性将用于刷新refresh_flush事件,并指示正在刷新的属性名称列表。


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


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


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

    mapper = User.__mapper__


    这个 Mapper 也是使用 inspect() 函数:

    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。要检索在查询此类期间实际选择的 “selectable”,可以通过 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,它包括所有映射的属性以及 hybrid、关联代理:

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


另请参阅


映射


检查 Map 实例


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>


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

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


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

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


属性状态信息,例如尚未加载的属性或 延迟加载(假设地址引用 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'])