SqlAlchemy 2.0 中文文档(四)(2)https://developer.aliyun.com/article/1562995
ORM 映射类概述
ORM 类映射配置概述。
对于对 SQLAlchemy ORM 和/或对 Python 比较新的读者来说,建议浏览 ORM 快速入门,最好是通过 SQLAlchemy 统一教程进行学习,其中首次介绍了 ORM 配置,即使用 ORM 声明形式定义表元数据。
ORM 映射风格
SQLAlchemy 具有两种不同的映射器配置风格,然后具有更多的子选项来设置它们。映射器风格的可变性存在是为了适应各种开发人员偏好的列表,包括用户定义的类与如何映射到关系模式表和列之间的抽象程度,正在使用的类层次结构的种类,包括是否存在自定义元类方案,最后,是否同时存在其他类实例化方法,例如是否同时使用 Python dataclasses。
在现代 SQLAlchemy 中,这些风格之间的差异基本上是表面的;当使用特定的 SQLAlchemy 配置风格来表达映射类的意图时,映射类的内部映射过程大部分都是相同的,最终的结果始终是一个用户定义的类,其配置了针对可选择单元的Mapper
,通常由Table
对象表示,并且该类本身已经被 instrumented 以包括与关系操作相关的行为,无论是在类的级别还是在该类的实例上。由于过程在所有情况下基本上都是相同的,因此从不同风格映射的类始终是完全可互操作的。协议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
对象,以及保留映射到类的一组Table
对象的MetaData
对象。
主要的声明式映射样式在以下各节中进一步详细说明:
- 使用声明基类 - 使用基类进行声明式映射。
- 使用装饰器的声明式映射(无声明基类) - 使用装饰器进行声明式映射,而不是使用基类。
在声明式映射类的范围内,Table
元数据的声明方式也有两种变体。包括:
- 使用 mapped_column() 的声明式表 - 在映射类内联声明表列,使用
mapped_column()
指令(或在传统形式中,直接使用Column
对象)。mapped_column()
指令也可以选择性地与使用Mapped
类的类型注解结合,该类可以直接提供有关映射列的一些详细信息。列指令,结合__tablename__
和可选的__table_args__
类级指令,将允许声明式映射过程构建要映射的Table
对象。 - 具有命令式表的声明式(又名混合声明式) - 不是单独指定表名和属性,而是将显式构建的
Table
对象与以其他方式进行声明式映射的类关联。这种映射风格是“声明式”和“命令式”映射的混合,并适用于将类映射到反射的Table
对象,以及将类映射到现有的 Core 构造,如连接和子查询。
声明式映射的文档继续在 使用声明式映射映射类 中。### 命令式映射
命令式或经典映射是指使用 registry.map_imperatively()
方法配置映射类的情况,其中目标类不包含任何声明类属性。
提示
命令式映射形式是 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
的参数来自给定的映射形式,包括传递给 Imperative 映射的registry.map_imperatively()
的参数,或者在使用声明式系统时,来自被映射的表列、SQL 表达式和关系以及 mapper_args 等属性的组合。
Mapper
类寻找的配置信息大致可以分为四类:
要映射的类
这是我们应用程序中构建的类。通常情况下,这个类的结构没有限制。[1] 当映射一个 Python 类时,对于这个类只能有一个Mapper
对象。[2]
当使用声明式映射样式进行映射时,要映射的类要么是声明基类的子类,要么由装饰器或函数(如registry.mapped()
)处理。
当使用命令式映射样式进行映射时,类直接作为map_imperatively.class_
参数传递。
表或其他来自子句对象
在绝大多数常见情况下,这是Table
的实例。对于更高级的用例,它也可以指任何一种FromClause
对象,最常见的替代对象是Subquery
和Join
对象。
当使用声明式映射样式时,主题表格是根据__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")
提示
声明式数据类映射功能提供了一种通过使用 Python 数据类生成默认__init__()
方法的替代方法,并允许高度可配置的构造函数形式。
警告
当对象在 Python 代码中构造时,仅在调用类的__init__()
方法时才会调用__init__()
方法,而不是在从数据库加载或刷新对象时。请参阅下一节在加载时保持非映射状态了解如何在加载对象时调用特殊逻辑的基础知识。
包含显式__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)
上述类,如 命令式映射 中所述的那样被命令式映射,还将具有与 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
,更类似于反序列化,比如反序列化,而不是初始构造。对象的大部分重要状态不是首次组装,而是重新从数据库行加载。
因此,为了在对象内部维护不属于存储到数据库的数据的状态,使得当对象加载和构造时都存在这些状态,有两种通用方法如下所述。
- 使用 Python 描述符,比如
@property
,而不是状态,根据需要动态计算属性。
对于简单的属性,这是最简单的方法,也是最不容易出错的方法。例如,如果一个对象Point
有Point.x
和Point.y
,想要一个这些属性的和的属性:
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
- 使用动态描述符的优点是值每次都会计算,这意味着它会根据底层属性(在本例中为
x
和y
)的更改来维护正确的值。
其他形式的上述模式包括 Python 标准库cached_property 装饰器(它是缓存的,并且不会每次重新计算),以及 SQLAlchemy 的hybrid_property
装饰器,允许属性在 SQL 查询中使用。 - 使用
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
属性将出现在refresh
和refresh_flush
事件中,并指示正在刷新的属性名称列表。### 映射类、实例和映射器的运行时内省
使用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
。要检索在查询此类时实际选择的“可选择”对象,可以通过Mapper.selectable
属性获取:
table = inspect(User).selectable
映射器对象的检查
如前一节所示,无论使用何种方法,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
#### Inspection of Mapped Instances
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>
关于对象的当前 persistence state 的信息:
>>> 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'])
另请参阅
InstanceState
InstanceState.attrs
AttributeState
## ORM Mapping Styles
SQLAlchemy 提供了两种不同风格的映射配置,然后进一步提供了设置它们的子选项。映射样式的可变性存在是为了适应开发者偏好的多样性,包括用户定义类与如何映射到关系模式表和列之间的抽象程度,使用的类层次结构种类,包括是否存在自定义元类方案,以及是否同时使用了其他类内部操作方法,例如是否同时使用了 Python dataclasses。
在现代 SQLAlchemy 中,这些风格之间的区别主要是表面的;当使用特定的 SQLAlchemy 配置风格来表达映射类的意图时,映射类的内部映射过程在大多数情况下是相同的,最终的结果总是一个用户定义的类,该类已经针对可选择的单元(通常由一个Table
对象表示)配置了一个Mapper
,并且该类本身已经被 instrumented 以包括与关系操作相关的行为,既在类的级别,也在该类的实例上。由于在所有情况下该过程基本相同,从不同风格映射的类始终是完全可互操作的。当使用诸如 mypy 等类型检查器时,可以使用协议MappedClassProtocol
来指示已映射的类。
最初的映射 API 通常被称为“古典”风格,而更自动化的映射风格则被称为“声明式”风格。SQLAlchemy 现在将这两种映射风格称为命令式映射和声明式映射。
无论使用何种映射样式,自 SQLAlchemy 1.4 起,所有 ORM 映射都源自一个名为registry
的单个对象,它是一组映射类的注册表。使用此注册表,一组映射配置可以作为一个组完成,并且在配置过程中,特定注册表中的类可以通过名称相互引用。
在 1.4 版本中更改:声明式和古典映射现在被称为“声明式”和“命令式”映射,并在内部统一,所有这些都源自代表相关映射的registry
构造。
SqlAlchemy 2.0 中文文档(四)(4)https://developer.aliyun.com/article/1562997