非传统映射


将 Class 映射到多个表


映射器可以针对任意关系单元(称为 可选)除了普通表格。例如,join() function 创建一个可选择的单位,该单位由 多个表,具有自己的复合主键,可以是 映射的方式与 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 表示 useraddress 表。user.idaddress.user_id columns 等同于外键,因此在 Map 中定义了它们 作为一个属性,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)useraddress 表组合在一起。 标识 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 结构,然后将一行 INSERT 到q_table表中,然后使用给定的 Connection 执行它,该连接与用于为 flush 过程发出其他 SQL 的 Connection 相同。用户提供的 logic 必须检测到从 “p” 到 “q” 的 LEFT OUTER JOIN 没有 “q” 端的条目。


针对任意子查询映射类


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

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。映射客户 class 添加到这个 selectable 中,然后创建一个类,该类将包含 那些属性。


当 ORM 保留 Customer 的新实例时,只有 customers 表实际上会收到一个 INSERT。这是因为 orders 表的主键未在 Map 中表示;ORM 只会向已映射主键的表中发出 INSERT。


注意


几乎不需要 Map 到任意 SELECT 语句的做法,尤其是如上所述的复杂语句;它必然倾向于产生复杂的查询,这些查询通常比直接查询构造产生的查询效率低。这种做法在某种程度上是基于 SQLAlchemy 的早期历史,其中 Mapper construct 用于表示主要的查询接口; 在现代用法中,Query 对象几乎可以用于构造任何 SELECT 语句,包括复杂的复合语句,并且应该优先于“map-to-selectable”方法。


一个类的多个映射器


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


“非主要”映射器的概念存在于 SQLAlchemy 的许多版本中,但是从版本 1.3 开始,此功能已被弃用。这种非主映射器有用的一种情况是,当针对替代可选对象构建与类的关系时。此用例现在适合使用别名构造,并在与别名类的关系中进行了介绍。


就一个类在不同场景下可以完全持久化到不同表的用例而言,非常早期的 SQLAlchemy 版本为此提供了一个改编自 Hibernate 的功能,称为“实体名称”功能。但是,一旦映射类本身成为 SQL 表达式构造的来源,此用例在 SQLAlchemy 中就变得不可行;也就是说,类的属性本身直接链接到映射的表列。该功能被删除并替换为一种简单的面向配方的方法来完成此任务,而不会有任何歧义的插桩 - 创建新的子类,每个子类都单独映射。此模式现在作为 Entity Name (实体名称) 中的配方提供。