非传统映射

针对多个表映射类

除了普通表之外,映射器还可以针对任意关系单元(称为可选择项)构建。例如,join() 函数创建一个由多个表组成的可选择单元,并带有自己的复合主键,可以像 Table 一样进行映射

from sqlalchemy import Table, Column, Integer, String, MetaData, join, ForeignKey
from sqlalchemy.orm import DeclarativeBase
from sqlalchemy.orm import column_property

metadata_obj = MetaData()

# define two Table objects
user_table = Table(
    "user",
    metadata_obj,
    Column("id", Integer, primary_key=True),
    Column("name", String),
)

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

# define a join between them.  This
# takes place across the user.id and address.user_id
# columns.
user_address_join = join(user_table, address_table)


class Base(DeclarativeBase):
    metadata = metadata_obj


# map to it
class AddressUser(Base):
    __table__ = user_address_join

    id = column_property(user_table.c.id, address_table.c.user_id)
    address_id = address_table.c.id

在上面的示例中,join 表达式表示 user 表和 address 表的列。user.idaddress.user_id 列通过外键相等,因此在映射中,它们被定义为一个属性 AddressUser.id,使用 column_property() 来指示专门的列映射。基于配置的这一部分,当 flush 发生时,映射会将新的主键值从 user.id 复制到 address.user_id 列中。

此外,address.id 列被显式映射到一个名为 address_id 的属性。这是为了消除歧义,将 address.id 列的映射与同名的 AddressUser.id 属性区分开来,后者在此被分配为引用 user 表以及 address.user_id 外键。

上述映射的自然主键是 (user.id, address.id) 的组合,因为这些是 user 表和 address 表组合在一起的主键列。AddressUser 对象的标识将根据这两个值来确定,并从 AddressUser 对象表示为 (AddressUser.id, AddressUser.address_id)

当引用 AddressUser.id 列时,大多数 SQL 表达式将仅使用映射列列表中的第一列,因为这两列是同义的。但是,对于特殊用例,例如 GROUP BY 表达式,其中必须同时引用这两列,同时利用适当的上下文,即适应别名和类似情况,可以使用访问器 Comparator.expressions

stmt = select(AddressUser).group_by(*AddressUser.id.expressions)

1.3.17 版本新增: 添加了 Comparator.expressions 访问器。

注意

如上所示,针对多个表的映射支持持久性,即针对目标表中的行的 INSERT、UPDATE 和 DELETE 操作。但是,它不支持同时对一个记录的其中一个表执行 UPDATE 操作,并对其他表执行 INSERT 或 DELETE 操作。也就是说,如果记录 PtoQ 映射到表“p”和“q”,其中它基于“p”和“q”的 LEFT OUTER JOIN 具有一行,如果 UPDATE 继续进行以更改现有记录中“q”表中的数据,则“q”中的行必须存在;如果主键标识已存在,它将不会发出 INSERT。如果该行不存在,对于大多数支持报告受 UPDATE 影响的行数的 DBAPI 驱动程序,ORM 将无法检测到更新的行并引发错误;否则,数据将被静默忽略。

允许即时“插入”相关行的配方可能使用 .MapperEvents.before_update 事件,如下所示

from sqlalchemy import event


@event.listens_for(PtoQ, "before_update")
def receive_before_update(mapper, connection, target):
    if target.some_required_attr_on_q is None:
        connection.execute(q_table.insert(), {"id": target.id})

在上面,通过使用 Table.insert() 创建 INSERT 构造,然后使用给定的 Connection 执行它,将行 INSERT 到 q_table 表中,该 Connection 与用于为 flush 进程发出其他 SQL 的连接相同。用户提供的逻辑必须检测到从“p”到“q”的 LEFT OUTER JOIN 没有“q”侧的条目。

针对任意子查询映射类

与针对 join 进行映射类似,一个普通的 select() 对象也可以与映射器一起使用。下面的示例片段说明了将名为 Customer 的类映射到 select(),其中包括与子查询的 join

from sqlalchemy import select, func

subq = (
    select(
        func.count(orders.c.id).label("order_count"),
        func.max(orders.c.price).label("highest_order"),
        orders.c.customer_id,
    )
    .group_by(orders.c.customer_id)
    .subquery()
)

customer_select = (
    select(customers, subq)
    .join_from(customers, subq, customers.c.id == subq.c.customer_id)
    .subquery()
)


class Customer(Base):
    __table__ = customer_select

上面,customer_select 表示的完整行将是 customers 表的所有列,以及 subq 子查询公开的那些列,它们是 order_counthighest_ordercustomer_id。然后,将 Customer 类映射到此可选择项会创建一个包含这些属性的类。

当 ORM 持久化 Customer 的新实例时,实际上只有 customers 表会接收 INSERT。这是因为 orders 表的主键未在映射中表示;ORM 只会对它已映射主键的表发出 INSERT。

注意

映射到任意 SELECT 语句(尤其是上面这种复杂的语句)的做法几乎是不需要的;它必然会产生复杂的查询,这些查询通常不如直接查询构造产生的查询有效。这种做法在某种程度上基于 SQLAlchemy 的早期历史,当时 Mapper 构造旨在表示主要的查询接口;在现代用法中,Query 对象可用于构造几乎任何 SELECT 语句,包括复杂的复合语句,并且应该优先于“映射到可选择项”的方法。

一个类的多个映射器

在现代 SQLAlchemy 中,一个特定的类一次只能由一个所谓的映射器映射。此映射器参与三个主要功能领域:查询、持久性和映射类的 instrumentation。主映射器的基本原理与 Mapper 修改类本身这一事实有关,不仅将其持久化到特定的 Table,而且还 instrumenting 类上的属性,这些属性根据表元数据专门构建。不可能有多个映射器以同等程度与一个类关联,因为只有一个映射器可以实际 instrument 类。

“非主”映射器的概念在 SQLAlchemy 的许多版本中都存在,但是从 1.3 版本开始,此功能已被弃用。这种非主映射器有用的一个案例是构建与类的关系以针对替代可选择项。现在,使用 aliased 构造来适应此用例,并在 与别名类的关系 中进行了描述。

至于一个类可以在不同场景下实际完全持久化到不同表中的用例,早期的 SQLAlchemy 版本为此提供了一个从 Hibernate 调整而来的功能,称为“实体名称”功能。但是,一旦映射类本身成为 SQL 表达式构造的来源,即类的属性本身直接链接到映射的表列,此用例在 SQLAlchemy 中就变得不可行。该功能被删除,并被一个简单的面向配方的方法所取代,以完成此任务而没有任何 instrumentation 的歧义 - 创建新的子类,每个子类单独映射。此模式现在作为配方在 实体名称 中可用。