SqlAlchemy-2-0-中文文档-二-

SqlAlchemy 2.0 中文文档(二)

原文:docs.sqlalchemy.org/en/20/contents.html

使用 UPDATE 和 DELETE 语句

原文:docs.sqlalchemy.org/en/20/tutorial/data_update.html

到目前为止,我们已经覆盖了 Insert,这样我们可以将一些数据放入我们的数据库中,并且花了很多时间在 Select 上,该语句处理了从数据库检索数据所使用的各种广泛的使用模式。 在本节中,我们将涵盖 UpdateDelete 构造,用于修改现有行以及删除现有行。 本节将从核心的角度讨论这些构造。

ORM 读者 - 正如在 使用 INSERT 语句 中提到的情况一样,当与 ORM 一起使用时,UpdateDelete 操作通常从 Session 对象内部作为 工作单元 进程的一部分调用。

然而,与 Insert 不同,UpdateDelete 构造也可以直接与 ORM 一起使用,使用一种称为“ORM-enabled update and delete”的模式;因此,熟悉这些构造对于 ORM 的使用很有用。 这两种使用方式在以下章节中讨论:使用工作单元模式更新 ORM 对象 和 使用工作单元模式删除 ORM 对象。

update() SQL 表达式构造

update() 函数生成一个 Update 的新实例,表示 SQL 中的 UPDATE 语句,该语句将更新表中的现有数据。

insert()构造类似,还有一种“传统”的update()形式,它一次只针对一个表发出 UPDATE,不返回任何行。然而,一些后端支持可以一次修改多个表的 UPDATE 语句,并且 UPDATE 语句也支持 RETURNING,使得匹配行中包含的列可以在结果集中返回。

一个基本的 UPDATE 看起来像:

>>> from sqlalchemy import update
>>> stmt = (
...     update(user_table)
...     .where(user_table.c.name == "patrick")
...     .values(fullname="Patrick the Star")
... )
>>> print(stmt)
UPDATE  user_account  SET  fullname=:fullname  WHERE  user_account.name  =  :name_1 

Update.values()方法控制 UPDATE 语句的 SET 元素的内容。这是由Insert构造共享的相同方法。参数通常可以使用列名称作为关键字参数传递。

UPDATE 支持所有主要的 SQL UPDATE 形式,包括针对表达式的更新,在其中我们可以利用Column表达式:

>>> stmt = update(user_table).values(fullname="Username: " + user_table.c.name)
>>> print(stmt)
UPDATE  user_account  SET  fullname=(:name_1  ||  user_account.name) 

为了在“executemany”上下文中支持 UPDATE,其中将对同一语句调用许多参数集,可以使用bindparam()构造来设置绑定参数;这些参数取代了通常放置文本值的位置:

>>> from sqlalchemy import bindparam
>>> stmt = (
...     update(user_table)
...     .where(user_table.c.name == bindparam("oldname"))
...     .values(name=bindparam("newname"))
... )
>>> with engine.begin() as conn:
...     conn.execute(
...         stmt,
...         [
...             {"oldname": "jack", "newname": "ed"},
...             {"oldname": "wendy", "newname": "mary"},
...             {"oldname": "jim", "newname": "jake"},
...         ],
...     )
BEGIN  (implicit)
UPDATE  user_account  SET  name=?  WHERE  user_account.name  =  ?
[...]  [('ed',  'jack'),  ('mary',  'wendy'),  ('jake',  'jim')]
<sqlalchemy.engine.cursor.CursorResult  object  at  0x...>
COMMIT 

可应用于 UPDATE 的其他技术包括:

相关更新

UPDATE 语句可以通过使用相关子查询中的其他表中的行来使用。子查询可以用于任何可以放置列表达式的地方:

>>> scalar_subq = (
...     select(address_table.c.email_address)
...     .where(address_table.c.user_id == user_table.c.id)
...     .order_by(address_table.c.id)
...     .limit(1)
...     .scalar_subquery()
... )
>>> update_stmt = update(user_table).values(fullname=scalar_subq)
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=(SELECT  address.email_address
FROM  address
WHERE  address.user_id  =  user_account.id  ORDER  BY  address.id
LIMIT  :param_1) 
```  ### UPDATE..FROM

一些数据库,如 PostgreSQL 和 MySQL,支持一种称为“UPDATE FROM”的语法,在特殊的 FROM 子句中可以直接声明附加表。当其他表位于语句的 WHERE 子句中时,此语法将隐式生成:

```py
>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
...     .values(fullname="Pat")
... )
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=:fullname  FROM  address
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  :email_address_1 

还有一种 MySQL 特定的语法,可以更新多个表。这要求我们在 VALUES 子句中引用Table对象,以便引用其他表:

>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
...     .values(
...         {
...             user_table.c.fullname: "Pat",
...             address_table.c.email_address: "pat@aol.com",
...         }
...     )
... )
>>> from sqlalchemy.dialects import mysql
>>> print(update_stmt.compile(dialect=mysql.dialect()))
UPDATE  user_account,  address
SET  address.email_address=%s,  user_account.fullname=%s
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  %s 
```  ### 参数有序更新

另一个仅适用于 MySQL 的行为是,UPDATE 的 SET 子句中参数的顺序实际上影响每个表达式的评估。对于这种用例,`Update.ordered_values()`方法接受一个元组序列,以便可以控制此顺序 [[2]](#id2):

```py
>>> update_stmt = update(some_table).ordered_values(
...     (some_table.c.y, 20), (some_table.c.x, some_table.c.y + 10)
... )
>>> print(update_stmt)
UPDATE  some_table  SET  y=:y,  x=(some_table.y  +  :y_1) 
```  ## delete() SQL 表达式构造

`delete()` 函数生成一个表示 SQL 中 DELETE 语句的新实例 `Delete`,该语句将从表中删除行。

从 API 视角来看,`delete()` 语句与 `update()` 构造非常相似,传统上不返回行,但在一些数据库后端上允许有 RETURNING 变体。

```py
>>> from sqlalchemy import delete
>>> stmt = delete(user_table).where(user_table.c.name == "patrick")
>>> print(stmt)
DELETE  FROM  user_account  WHERE  user_account.name  =  :name_1 

多表删除

Update 类似,Delete 支持在 WHERE 子句中使用相关子查询以及后端特定的多表语法,例如 MySQL 上的 DELETE FROM..USING

>>> delete_stmt = (
...     delete(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
... )
>>> from sqlalchemy.dialects import mysql
>>> print(delete_stmt.compile(dialect=mysql.dialect()))
DELETE  FROM  user_account  USING  user_account,  address
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  %s 
```  ## 从 UPDATE、DELETE 中获取受影响的行数

`Update` 和 `Delete` 都支持在语句执行后返回匹配行数的功能,对于使用 Core `Connection` 调用的语句,即 `Connection.execute()`。根据下面提到的注意事项,这个值可以从 `CursorResult.rowcount` 属性中获取:

```py
>>> with engine.begin() as conn:
...     result = conn.execute(
...         update(user_table)
...         .values(fullname="Patrick McStar")
...         .where(user_table.c.name == "patrick")
...     )
...     print(result.rowcount)
BEGIN  (implicit)
UPDATE  user_account  SET  fullname=?  WHERE  user_account.name  =  ?
[...]  ('Patrick McStar',  'patrick')
1
COMMIT 

提示

CursorResult 类是 Result 的子类,其中包含特定于 DBAPI cursor 对象的附加属性。当通过 Connection.execute() 方法调用语句时,将返回此子类的实例。在使用 ORM 时,对所有 INSERT、UPDATE 和 DELETE 语句使用 Session.execute() 方法会返回此类型的对象。

关于 CursorResult.rowcount 的事实:

  • 返回的值是由语句的 WHERE 子句匹配的行数。无论实际上是否修改了行都无关紧要。

  • 对于使用 RETURNING 的 UPDATE 或 DELETE 语句,或者使用 executemany 执行的 UPDATE 或 DELETE 语句,不一定可以使用 CursorResult.rowcount。其可用性取决于正在使用的 DBAPI 模块。

  • 在任何 DBAPI 不能确定某种类型语句的行数的情况下,返回值将为 -1

  • SQLAlchemy 在关闭游标之前预先缓存 DBAPI 的 cursor.rowcount 值,因为某些 DBAPI 不支持事后访问此属性。为了为不是 UPDATE 或 DELETE 的语句(如 INSERT 或 SELECT)预先缓存 cursor.rowcount,可以使用 Connection.execution_options.preserve_rowcount 执行选项。

  • 一些驱动程序,特别是用于非关系型数据库的第三方方言,可能根本不支持 CursorResult.rowcountCursorResult.supports_sane_rowcount 游标属性会指示此情况。

  • “rowcount” 被 ORM 工作单元 过程用于验证 UPDATE 或 DELETE 语句是否匹配了预期数量的行,并且也是 ORM 版本控制功能的重要组成部分,该功能在 配置版本计数器 中有文档说明。

使用 UPDATE、DELETE 与 RETURNING

Insert 构造类似,UpdateDelete 也支持 RETURNING 子句,通过使用 Update.returning()Delete.returning() 方法添加。当这些方法在支持 RETURNING 的后端上使用时,与语句的 WHERE 条件匹配的所有行的选定列将作为可以迭代的行返回到 Result 对象中:

>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.name == "patrick")
...     .values(fullname="Patrick the Star")
...     .returning(user_table.c.id, user_table.c.name)
... )
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=:fullname
WHERE  user_account.name  =  :name_1
RETURNING  user_account.id,  user_account.name
>>> delete_stmt = (
...     delete(user_table)
...     .where(user_table.c.name == "patrick")
...     .returning(user_table.c.id, user_table.c.name)
... )
>>> print(delete_stmt)
DELETE  FROM  user_account
WHERE  user_account.name  =  :name_1
RETURNING  user_account.id,  user_account.name 

更新、删除的进一步阅读

另请参阅

更新/删除的 API 文档:

  • 更新

  • Delete

ORM 启用的 UPDATE 和 DELETE:

ORM-启用的 INSERT、UPDATE 和 DELETE 语句 - 在 ORM 查询指南 中

update() SQL 表达式构造

update() 函数生成一个新的 Update 实例,表示 SQL 中的 UPDATE 语句,将更新表中的现有数据。

insert() 构造一样,还有一个“传统”形式的 update(),它一次针对单个表发出 UPDATE,并且不返回任何行。然而,一些后端支持一种可以一次修改多个表的 UPDATE 语句,并且 UPDATE 语句还支持 RETURNING,以便匹配行中包含的列可以在结果集中返回。

基本的 UPDATE 如下所示:

>>> from sqlalchemy import update
>>> stmt = (
...     update(user_table)
...     .where(user_table.c.name == "patrick")
...     .values(fullname="Patrick the Star")
... )
>>> print(stmt)
UPDATE  user_account  SET  fullname=:fullname  WHERE  user_account.name  =  :name_1 

Update.values() 方法控制 UPDATE 语句的 SET 元素的内容。这是由 Insert 构造共享的相同方法。通常可以使用列名作为关键字参数传递参数。

UPDATE 支持所有主要的 SQL UPDATE 形式,包括针对表达式的更新,我们可以利用 Column 表达式:

>>> stmt = update(user_table).values(fullname="Username: " + user_table.c.name)
>>> print(stmt)
UPDATE  user_account  SET  fullname=(:name_1  ||  user_account.name) 

为了支持在“executemany”上下文中的 UPDATE,其中将针对同一语句调用许多参数集,可以使用 bindparam() 构造来设置绑定参数;这些参数替换了通常放置字面值的位置:

>>> from sqlalchemy import bindparam
>>> stmt = (
...     update(user_table)
...     .where(user_table.c.name == bindparam("oldname"))
...     .values(name=bindparam("newname"))
... )
>>> with engine.begin() as conn:
...     conn.execute(
...         stmt,
...         [
...             {"oldname": "jack", "newname": "ed"},
...             {"oldname": "wendy", "newname": "mary"},
...             {"oldname": "jim", "newname": "jake"},
...         ],
...     )
BEGIN  (implicit)
UPDATE  user_account  SET  name=?  WHERE  user_account.name  =  ?
[...]  [('ed',  'jack'),  ('mary',  'wendy'),  ('jake',  'jim')]
<sqlalchemy.engine.cursor.CursorResult  object  at  0x...>
COMMIT 

可应用于 UPDATE 的其他技术包括:

相关更新

UPDATE 语句可以通过使用 相关子查询 来使用其他表中的行。子查询可以在任何可以放置列表达式的地方使用:

>>> scalar_subq = (
...     select(address_table.c.email_address)
...     .where(address_table.c.user_id == user_table.c.id)
...     .order_by(address_table.c.id)
...     .limit(1)
...     .scalar_subquery()
... )
>>> update_stmt = update(user_table).values(fullname=scalar_subq)
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=(SELECT  address.email_address
FROM  address
WHERE  address.user_id  =  user_account.id  ORDER  BY  address.id
LIMIT  :param_1) 
```  ### UPDATE..FROM

一些数据库,如 PostgreSQL 和 MySQL,支持“UPDATE FROM”语法,其中额外的表可以直接在特殊的 FROM 子句中声明。当额外的表位于语句的 WHERE 子句中时,将隐式生成此语法:

```py
>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
...     .values(fullname="Pat")
... )
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=:fullname  FROM  address
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  :email_address_1 

还有一种 MySQL 特定的语法可以更新多个表。这需要在 VALUES 子句中引用Table对象,以便引用其他表:

>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
...     .values(
...         {
...             user_table.c.fullname: "Pat",
...             address_table.c.email_address: "pat@aol.com",
...         }
...     )
... )
>>> from sqlalchemy.dialects import mysql
>>> print(update_stmt.compile(dialect=mysql.dialect()))
UPDATE  user_account,  address
SET  address.email_address=%s,  user_account.fullname=%s
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  %s 
```  ### 参数排序更新

另一个仅适用于 MySQL 的行为是,UPDATE 的 SET 子句中参数的顺序实际上影响每个表达式的评估。对于这种用例,`Update.ordered_values()`方法接受一个元组序列,以便可以控制此顺序 [[2]](#id2):

```py
>>> update_stmt = update(some_table).ordered_values(
...     (some_table.c.y, 20), (some_table.c.x, some_table.c.y + 10)
... )
>>> print(update_stmt)
UPDATE  some_table  SET  y=:y,  x=(some_table.y  +  :y_1) 
```  ### 相关更新

UPDATE 语句可以通过使用相关子查询中的行来使用其他表中的行。子查询可以在任何可以放置列表达式的地方使用:

```py
>>> scalar_subq = (
...     select(address_table.c.email_address)
...     .where(address_table.c.user_id == user_table.c.id)
...     .order_by(address_table.c.id)
...     .limit(1)
...     .scalar_subquery()
... )
>>> update_stmt = update(user_table).values(fullname=scalar_subq)
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=(SELECT  address.email_address
FROM  address
WHERE  address.user_id  =  user_account.id  ORDER  BY  address.id
LIMIT  :param_1) 

UPDATE..FROM

一些数据库,如 PostgreSQL 和 MySQL,支持“UPDATE FROM”语法,其中额外的表可以直接在特殊的 FROM 子句中声明。当额外的表位于语句的 WHERE 子句中时,此语法将隐式生成:

>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
...     .values(fullname="Pat")
... )
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=:fullname  FROM  address
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  :email_address_1 

还有一种 MySQL 特定的语法可以更新多个表。这需要在 VALUES 子句中引用Table对象,以便引用其他表:

>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
...     .values(
...         {
...             user_table.c.fullname: "Pat",
...             address_table.c.email_address: "pat@aol.com",
...         }
...     )
... )
>>> from sqlalchemy.dialects import mysql
>>> print(update_stmt.compile(dialect=mysql.dialect()))
UPDATE  user_account,  address
SET  address.email_address=%s,  user_account.fullname=%s
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  %s 

参数排序更新

另一个仅适用于 MySQL 的行为是,UPDATE 的 SET 子句中参数的顺序实际上影响每个表达式的评估。对于这种用例,Update.ordered_values()方法接受一个元组序列,以便可以控制此顺序 [2]

>>> update_stmt = update(some_table).ordered_values(
...     (some_table.c.y, 20), (some_table.c.x, some_table.c.y + 10)
... )
>>> print(update_stmt)
UPDATE  some_table  SET  y=:y,  x=(some_table.y  +  :y_1) 

delete() SQL 表达式构造

delete()函数生成一个新的Delete实例,表示 SQL 中的 DELETE 语句,它将从表中删除行。

delete()语句从 API 的角度来看与update()构造非常相似,传统上不返回任何行,但在一些数据库后端上允许使用 RETURNING 变体。

>>> from sqlalchemy import delete
>>> stmt = delete(user_table).where(user_table.c.name == "patrick")
>>> print(stmt)
DELETE  FROM  user_account  WHERE  user_account.name  =  :name_1 

多表删除

Update一样,Delete支持在 WHERE 子句中使用相关子查询,以及后端特定的多表语法,例如 MySQL 上的DELETE FROM..USING

>>> delete_stmt = (
...     delete(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
... )
>>> from sqlalchemy.dialects import mysql
>>> print(delete_stmt.compile(dialect=mysql.dialect()))
DELETE  FROM  user_account  USING  user_account,  address
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  %s 
```  ### 多表删除

与`Update`类似,`Delete`也支持在 WHERE 子句中使用相关子查询,以及后端特定的多表语法,例如在 MySQL 上的 `DELETE FROM..USING`:

```py
>>> delete_stmt = (
...     delete(user_table)
...     .where(user_table.c.id == address_table.c.user_id)
...     .where(address_table.c.email_address == "patrick@aol.com")
... )
>>> from sqlalchemy.dialects import mysql
>>> print(delete_stmt.compile(dialect=mysql.dialect()))
DELETE  FROM  user_account  USING  user_account,  address
WHERE  user_account.id  =  address.user_id  AND  address.email_address  =  %s 

从 UPDATE、DELETE 获取受影响的行数

UpdateDelete 都支持在语句执行后返回匹配的行数的功能,对于使用 Core Connection 调用的语句,即 Connection.execute()。根据下面提到的注意事项,此值可从 CursorResult.rowcount 属性中获取:

>>> with engine.begin() as conn:
...     result = conn.execute(
...         update(user_table)
...         .values(fullname="Patrick McStar")
...         .where(user_table.c.name == "patrick")
...     )
...     print(result.rowcount)
BEGIN  (implicit)
UPDATE  user_account  SET  fullname=?  WHERE  user_account.name  =  ?
[...]  ('Patrick McStar',  'patrick')
1
COMMIT 

提示

CursorResult 类是 Result 的子类,它包含特定于 DBAPI cursor 对象的其他属性。当通过 Connection.execute() 方法调用语句时,将返回此子类的实例。在使用 ORM 时,Session.execute() 方法为所有 INSERT、UPDATE 和 DELETE 语句返回此类型的对象。

有关 CursorResult.rowcount 的事实:

  • 返回的值是由语句的 WHERE 子句匹配的行数。无论实际上是否修改了行都无关紧要。

  • CursorResult.rowcount 对于使用 RETURNING 的 UPDATE 或 DELETE 语句,或者使用 executemany 执行的语句未必可用。可用性取决于所使用的 DBAPI 模块。

  • 在 DBAPI 未确定某种类型语句的行数的任何情况下,返回值都将是 -1

  • SQLAlchemy 在游标关闭之前预先缓存 DBAPIs cursor.rowcount 的值,因为某些 DBAPIs 不支持在事后访问此属性。为了为非 UPDATE 或 DELETE 的语句(例如 INSERT 或 SELECT)预先缓存 cursor.rowcount,可以使用 Connection.execution_options.preserve_rowcount 执行选项。

  • 一些驱动程序,特别是非关系数据库的第三方方言,可能根本不支持 CursorResult.rowcountCursorResult.supports_sane_rowcount 游标属性将指示这一点。

  • “rowcount” 被 ORM 工作单元 过程用于验证 UPDATE 或 DELETE 语句是否匹配预期的行数,并且还是 ORM 版本控制功能的关键,该功能在 配置版本计数器 中有文档记录。

使用 RETURNING 与 UPDATE、DELETE

Insert 构造相似,UpdateDelete 也支持通过使用 Update.returning()Delete.returning() 方法添加的 RETURNING 子句。当这些方法在支持 RETURNING 的后端上使用时,匹配 WHERE 条件的所有行的选定列将作为可迭代的行返回到 Result 对象中:

>>> update_stmt = (
...     update(user_table)
...     .where(user_table.c.name == "patrick")
...     .values(fullname="Patrick the Star")
...     .returning(user_table.c.id, user_table.c.name)
... )
>>> print(update_stmt)
UPDATE  user_account  SET  fullname=:fullname
WHERE  user_account.name  =  :name_1
RETURNING  user_account.id,  user_account.name
>>> delete_stmt = (
...     delete(user_table)
...     .where(user_table.c.name == "patrick")
...     .returning(user_table.c.id, user_table.c.name)
... )
>>> print(delete_stmt)
DELETE  FROM  user_account
WHERE  user_account.name  =  :name_1
RETURNING  user_account.id,  user_account.name 

关于 UPDATE、DELETE 的进一步阅读

请参阅

UPDATE / DELETE 的 API 文档:

  • Update

  • Delete

启用 ORM 的 UPDATE 和 DELETE:

ORM 支持的 INSERT、UPDATE 和 DELETE 语句 - 在 ORM 查询指南 中

使用 ORM 进行数据操作

原文:docs.sqlalchemy.org/en/20/tutorial/orm_data_manipulation.html

上一节处理数据保持了从核心角度来看 SQL 表达语言的关注,以便提供各种主要 SQL 语句结构的连续性。接下来的部分将扩展Session的生命周期,以及它如何与这些结构交互。

先决条件部分 - 教程中 ORM 重点部分建立在本文档中的两个先前 ORM 中心部分的基础上:

  • 使用 ORM 会话执行 - 介绍如何创建 ORM Session对象

  • 使用 ORM 声明性表单定义表元数据 - 我们在这里设置了UserAddress实体的 ORM 映射

  • 选择 ORM 实体和列 - 一些关于如何为诸如User之类的实体运行 SELECT 语句的示例

使用 ORM 工作单元模式插入行

当使用 ORM 时,Session对象负责构造Insert构造并将它们作为 INSERT 语句发出到正在进行的事务中。我们指示Session这样做的方式是通过添加对象条目到它; Session然后确保这些新条目在需要时被发出到数据库,使用称为flush的过程。Session用于持久化对象的整体过程被称为工作单元模式。

类的实例代表行

而在前一个示例中,我们使用 Python 字典发出了一个 INSERT,以指示我们要添加的数据,使用 ORM 时,我们直接使用我们定义的自定义 Python 类,在使用 ORM 声明性表单定义表元数据中。在类级别,UserAddress类用作定义相应数据库表应该如何查看的位置。这些类还用作可扩展的数据对象,我们用它们来创建和操作事务中的行。下面我们将创建两个User对象,每个对象代表一个要插入的潜在数据库行:

>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

我们可以使用映射列的名称作为构造函数中的关键字参数来构造这些对象。这是可能的,因为 User 类包含一个由 ORM 映射提供的自动生成的 __init__() 构造函数,以便我们可以使用构造函数中的列名作为键来创建每个对象。

类似于我们在 Insert 的核心示例中的做法,我们没有包含主键(即 id 列的条目),因为我们希望利用数据库的自动递增主键功能,这里是 SQLite,ORM 也与之集成。上述对象的 id 属性的值,如果我们查看它,会显示为 None

>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

None 值由 SQLAlchemy 提供,表示属性目前没有值。在处理尚未分配值的新对象时,SQLAlchemy 映射的属性始终在 Python 中返回一个值,并且如果缺少值,则不会引发 AttributeError

目前,上述两个对象被称为处于 transient 状态 - 它们与任何数据库状态都没有关联,尚未与可以为它们生成 INSERT 语句的 Session 对象关联。

将对象添加到会话

为了逐步说明添加过程,我们将创建一个不使用上下文管理器的 Session(因此我们必须确保稍后关闭它!):

>>> session = Session(engine)

然后使用 Session.add() 方法将对象添加到 Session 中。当调用此方法时,对象处于一种称为 pending 的状态,尚未插入:

>>> session.add(squidward)
>>> session.add(krabs)

当我们有待处理的对象时,我们可以通过查看 Session 上的一个集合来查看这种状态,该集合称为 Session.new

>>> session.new
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])

上述视图使用一个名为 IdentitySet 的集合,它本质上是一个 Python 集合,以所有情况下的对象标识哈希(即使用 Python 内置的 id() 函数,而不是 Python 的 hash() 函数)。

刷新

Session 使用一种称为工作单元(unit of work)的模式。这通常意味着它逐个累积更改,但实际上直到需要时才将它们传递到数据库。这使它能够根据给定的一组待处理更改,更好地决定如何在事务中发出 SQL DML。当它确实向数据库发出 SQL 以推送当前更改集时,该过程被称为刷新

我们可以通过调用Session.flush()方法来手动说明刷新过程:

>>> session.flush()
BEGIN  (implicit)
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  ('squidward',  'Squidward Tentacles')
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  ('ehkrabs',  'Eugene H. Krabs') 

上面我们观察到首先调用Session以发出 SQL,因此它创建了一个新的事务并为两个对象发出了适当的 INSERT 语句。事务现在保持打开,直到我们调用任何Session.commit()Session.rollback()Session.close()方法。

虽然Session.flush()可用于手动推送待处理更改到当前事务,但通常是不必要的,因为Session具有一种称为自动刷新的行为,我们稍后将说明。每当调用Session.commit()时,它也会刷新更改。

自动产生的主键属性

一旦行被插入,我们创建的两个 Python 对象处于持久(persistent)状态,它们与它们被添加或加载到的Session对象相关联,并具有稍后将介绍的许多其他行为。

发生的 INSERT 的另一个效果是 ORM 检索了每个新对象的新主键标识符;在内部,它通常使用我们之前介绍的相同的CursorResult.inserted_primary_key访问器。squidwardkrabs 对象现在具有这些新的主键标识符,并且我们可以通过访问 id 属性查看它们:

>>> squidward.id
4
>>> krabs.id
5

提示

当 ORM 在刷新对象时为什么会发出两个单独的 INSERT 语句,而不是使用 executemany?正如我们将在下一节中看到的,Session在刷新对象时始终需要知道新插入对象的主键。如果使用了诸如 SQLite 的自动增量(其他示例包括 PostgreSQL IDENTITY 或 SERIAL,使用序列等)之类的功能,则CursorResult.inserted_primary_key功能通常要求每次 INSERT 都逐行发出。如果我们提前为主键提供了值,ORM 将能够更好地优化操作。一些数据库后端,如 psycopg2,还可以一次插入多行,同时仍然能够检索主键值。

通过主键从身份映射获取对象

对象的主键标识对于Session非常重要,因为这些对象现在使用称为身份映射的功能与此标识在内存中连接在一起。身份映射是一个内存存储器,它将当前加载在内存中的所有对象与它们的主键标识链接起来。我们可以通过使用Session.get()方法之一来检索上述对象之一来观察到这一点,如果本地存在,则返回身份映射中的条目,否则发出一个 SELECT:

>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

身份映射的重要一点是,在特定Session对象的范围内,它维护着特定 Python 对象的唯一实例与特定数据库标识的关系。我们可以观察到,some_squidward指的是之前squidward所指的同一个对象

>>> some_squidward is squidward
True

身份映射是一个关键特性,允许在事务中操作复杂的对象集合而不会出现同步问题。

提交

关于Session的工作方式还有很多要说的,这将在后续进一步讨论。现在我们将提交事务,以便在深入研究 ORM 行为和特性之前积累关于如何在 SELECT 行之前的知识:

>>> session.commit()
COMMIT

上述操作将提交正在进行的事务。 我们处理过的对象仍然附加到 Session,这是一个状态,直到 Session关闭(在关闭会话中介绍)。

提示

注意的一件重要事情是,我们刚刚处理的对象上的属性已经过期,意味着,当我们下一次访问它们的任何属性时,Session 将启动一个新的事务并重新加载它们的状态。 这个选项有时对性能原因或者如果希望在关闭Session后继续使用对象(这被称为分离状态)可能会有问题,因为它们将不会有任何状态,并且将没有 Session 与其一起加载该状态,导致“分离实例”错误。 可以使用一个名为Session.expire_on_commit的参数来控制行为。 更多信息请参见关闭会话。 ## 使用工作单元模式更新 ORM 对象

在前面的一节使用 UPDATE 和 DELETE 语句中,我们介绍了代表 SQL UPDATE 语句的 Update 构造。 当使用 ORM 时,有两种方式使用此构造。 主要方式是,它作为Session使用的工作单元过程的一部分自动发出,其中对具有更改的单个对象对应的每个主键发出一个 UPDATE 语句。

假设我们将用户名为sandyUser对象加载到一个事务中(同时还展示了Select.filter_by()方法以及Result.scalar_one()方法):

>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
BEGIN  (implicit)
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('sandy',) 

如前所述,Python 对象sandy充当数据库中的行的代理,更具体地说,是相对于当前事务的具有主键标识2的数据库行:

>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')

如果我们更改此对象的属性,Session将跟踪此更改:

>>> sandy.fullname = "Sandy Squirrel"

对象出现在一个名为Session.dirty的集合中,表示对象“脏”:

>>> sandy in session.dirty
True

Session下次执行 flush 时,将会发出一个 UPDATE,以在数据库中更新此值。如前所述,在发出任何 SELECT 之前,会自动执行 flush,这种行为称为自动 flush。我们可以直接查询这一行的User.fullname列,我们将得到我们的更新值:

>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
UPDATE  user_account  SET  fullname=?  WHERE  user_account.id  =  ?
[...]  ('Sandy Squirrel',  2)
SELECT  user_account.fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (2,)
>>> print(sandy_fullname)
Sandy Squirrel

我们可以看到上面我们请求Session执行了一个单独的select()语句。然而,发出的 SQL 显示了还发出了一个 UPDATE,这是 flush 过程推出挂起的更改。sandy Python 对象现在不再被认为是脏的:

>>> sandy in session.dirty
False

然而请注意,我们仍然处于一个事务中,我们的更改尚未推送到数据库的永久存储中。由于桑迪的姓实际上是“Cheeks”而不是“Squirrel”,我们将在回滚事务时修复这个错误。但首先我们会做一些更多的数据更改。

亦可参见

Flush-详细说明了 flush 过程以及关于Session.autoflush设置的信息。##使用工作单元模式删除 ORM 对象

为了完成基本的持久性操作,可以使用Session.delete()方法在工作单元过程中标记一个个别的 ORM 对象以进行删除操作。让我们从数据库加载patrick

>>> patrick = session.get(User, 3)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,
user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (3,) 

如果我们标记patrick以进行删除,与其他操作一样,直到进行 flush 才会实际发生任何事情:

>>> session.delete(patrick)

当前的 ORM 行为是patrick会一直留在Session中,直到 flush 进行,如前所述,如果我们发出查询,就会发生 flush:

>>> session.execute(select(User).where(User.name == "patrick")).first()
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,
address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (3,)
DELETE  FROM  user_account  WHERE  user_account.id  =  ?
[...]  (3,)
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('patrick',) 

在上面,我们要求发出的 SELECT 语句之前是一个 DELETE,这表明 patrick 的待删除操作已经进行了。还有一个针对 address 表的 SELECT,这是由于 ORM 在寻找与目标行可能相关的这个表中的行而引起的;这种行为是所谓的 级联 行为的一部分,并且可以通过允许数据库自动处理 address 中的相关行来更有效地工作;关于此的详细信息请参见 delete。

另请参见

delete - 描述了如何调整 Session.delete() 的行为,以便处理其他表中的相关行应该如何处理。

除此之外,现在正在被删除的 patrick 对象实例不再被视为在 Session 中持久存在,这可以通过包含性检查来显示:

>>> patrick in session
False

然而,就像我们对 sandy 对象进行的更新一样,我们在这里做出的每一个改变都只在正在进行的事务中有效,如果我们不提交事务,这些改变就不会永久保存。由于此刻回滚事务更加有趣,我们将在下一节中进行。## 批量/多行 INSERT、upsert、UPDATE 和 DELETE

本节讨论的工作单元技术旨在将 dml(即 INSERT/UPDATE/DELETE 语句)与 Python 对象机制集成,通常涉及到相互关联对象的复杂图。一旦对象使用 Session.add() 添加到 Session 中,工作单元过程会自动代表我们发出 INSERT/UPDATE/DELETE,因为我们的对象属性被创建和修改。

但是,ORM Session 也有处理命令的能力,使其能够直接发出 INSERT、UPDATE 和 DELETE 语句,而不需要传递任何 ORM 持久化的对象,而是传递要 INSERT、UPDATE 或 upsert 的值列表,或者 WHERE 条件,以便可以调用一次匹配多行的 UPDATE 或 DELETE 语句。当需要影响大量行而无需构建和操作映射对象时,这种用法尤为重要,因为对于简单、性能密集型的任务,如大批量插入,这可能是繁琐和不必要的。

ORM 的批量/多行功能Session直接使用insert()update()delete()构造,并且它们的使用方式类似于与 SQLAlchemy Core 一起使用它们的方式(首次在本教程中介绍于使用 INSERT 语句和使用 UPDATE 和 DELETE 语句)。当使用这些构造与 ORMSession而不是普通的Connection时,它们的构建、执行和结果处理与 ORM 完全集成。

关于使用这些功能的背景和示例,请参见 ORM-启用的 INSERT、UPDATE 和 DELETE 语句部分,位于 ORM 查询指南中。

另请参阅

ORM-启用的 INSERT、UPDATE 和 DELETE 语句 - 在 ORM 查询指南中

回滚

Session有一个Session.rollback()方法,如预期般在进行中的 SQL 连接上发出 ROLLBACK。但是,它还会影响当前与Session关联的对象,例如我们先前示例中的 Python 对象sandy。虽然我们将sandy对象的.fullname更改为读取"Sandy Squirrel",但我们想要回滚此更改。调用Session.rollback()不仅会回滚事务,还会过期与此Session当前关联的所有对象,这将使它们在下次使用时自动刷新,使用一种称为延迟加载的过程:

>>> session.rollback()
ROLLBACK

要更仔细地查看“过期”过程,我们可以观察到 Python 对象sandy在其 Python__dict__中没有留下状态,除了一个特殊的 SQLAlchemy 内部状态对象:

>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

这是“过期”状态;再次访问属性将自动开始一个新的事务,并使用当前数据库行刷新sandy

>>> sandy.fullname
BEGIN  (implicit)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,
user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (2,)
'Sandy Cheeks'

我们现在可以观察到完整的数据库行也被填充到sandy对象的__dict__中:

>>> sandy.__dict__  
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

对于删除的对象,当我们之前注意到patrick不再在会话中时,该对象的身份也被恢复:

>>> patrick in session
True

当然,数据库数据也再次出现了:

>>> session.execute(select(User).where(User.name == "patrick")).scalar_one() is patrick
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('patrick',)
True

关闭会话

在上述部分中,我们在 Python 上下文管理器之外使用了一个Session对象,也就是说,我们没有使用with语句。这没问题,但是如果我们以这种方式操作,最好在完成后明确关闭Session

>>> session.close()
ROLLBACK 

关闭Session,也就是当我们在上下文管理器中使用它时发生的情况,会实现以下几个目标:

  • 它释放所有连接资源到连接池中,取消(例如回滚)任何正在进行的事务。

    这意味着当我们使用一个会话执行一些只读任务然后关闭它时,我们不需要显式调用Session.rollback()来确保事务被回滚;连接池会处理这个问题。

  • 清除Session中的所有对象。

    这意味着我们为这个Session加载的所有 Python 对象,比如sandypatricksquidward,现在处于称为分离的状态。特别是,我们会注意到仍处于过期状态的对象,例如由于调用了Session.commit(),现在已经不可用,因为它们不包含当前行的状态,并且不再与任何数据库事务相关联,也不再可以被刷新:

    # note that 'squidward.name' was just expired previously, so its value is unloaded
    >>> squidward.name
    Traceback (most recent call last):
      ...
    sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed
    

    分离的对象可以使用Session.add()方法重新与相同或新的Session关联,这将重新建立它们与特定数据库行的关系:

    >>> session.add(squidward)
    >>> squidward.name
    BEGIN  (implicit)
    SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,  user_account.fullname  AS  user_account_fullname
    FROM  user_account
    WHERE  user_account.id  =  ?
    [...]  (4,)
    'squidward'
    

    提示

    尽量避免使用对象处于分离状态。当关闭 Session 时,也清理对所有先前附加对象的引用。对于需要分离对象的情况,通常是在 Web 应用程序中立即显示刚提交的对象的情况下,其中 Session 在渲染视图之前关闭,在这种情况下,将 Session.expire_on_commit 标志设置为 False。## 使用 ORM 工作单元模式插入行

在使用 ORM 时,Session 对象负责构造 Insert 构造,并在进行中的事务中发出它们作为 INSERT 语句。我们指示 Session 这样做的方式是通过向其中添加对象条目;然后,Session 确保这些新条目在需要时将被发出到数据库中,使用一种称为 flush 的过程。Session 用于持久化对象的整体过程称为 工作单元 模式。

类的实例代表行

而在上一个示例中,我们使用 Python 字典发出了一个 INSERT,以指示我们要添加的数据,使用 ORM 时,我们直接使用我们在 使用 ORM 声明性表单定义表元数据 中定义的自定义 Python 类。在类级别上,UserAddress 类充当了定义相应数据库表应该如何的地方。这些类还作为可扩展的数据对象,我们用它来在事务中创建和操作行。下面我们将创建两个 User 对象,每个对象代表一个待插入的潜在数据库行:

>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

我们可以使用映射列的名称作为构造函数中的关键字参数来构造这些对象。这是可能的,因为 User 类包含了由 ORM 映射提供的自动生成的 __init__() 构造函数,以便我们可以使用列名作为构造函数中的键来创建每个对象。

与我们 Core 示例中的Insert类似,我们没有包含主键(即id列的条目),因为我们希望利用数据库的自动递增主键特性,此处为 SQLite,ORM 也与之集成。如果我们要查看上述对象的id属性的值,则显示为None

>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

None值由 SQLAlchemy 提供,以指示属性目前尚无值。SQLAlchemy 映射的属性始终在 Python 中返回一个值,并且在处理尚未分配值的新对象时不会引发AttributeError

目前,我们上述的两个对象被称为 transient 状态 - 它们与任何数据库状态都没有关联,尚未关联到可以为它们生成 INSERT 语句的Session对象。

添加对象到会话

为了逐步说明添加过程,我们将创建一个不使用上下文管理器的Session(因此我们必须确保稍后关闭它!):

>>> session = Session(engine)

然后使用Session.add()方法将对象添加到Session中。当调用此方法时,对象处于称为 pending 的状态,尚未插入:

>>> session.add(squidward)
>>> session.add(krabs)

当我们有待处理的对象时,我们可以通过查看Session上的一个集合来查看此状态,该集合称为Session.new

>>> session.new
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])

上述视图使用了一个名为IdentitySet的集合,实质上是一个 Python 集合,以所有情况下的对象标识进行哈希(即使用 Python 内置的id()函数,而不是 Python 的hash()函数)。

刷新

Session使用一种称为 unit of work 的模式。这通常意味着它逐一累积更改,但实际上直到需要才会将它们传达到数据库。这允许它根据给定的一组待处理更改做出有关在事务中应该发出 SQL DML 的更好决策。当它发出 SQL 到数据库以推出当前一组更改时,该过程称为flush

我们可以通过手动调用Session.flush()方法来说明刷新过程:

>>> session.flush()
BEGIN  (implicit)
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  ('squidward',  'Squidward Tentacles')
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  ('ehkrabs',  'Eugene H. Krabs') 

在上面的例子中,Session首先被调用以发出 SQL,因此它创建了一个新事务,并为两个对象发出了适当的 INSERT 语句。直到我们调用Session.commit()Session.rollback()Session.close()方法之一,该事务现在保持打开状态Session

虽然Session.flush()可以用于手动推送当前事务中的待处理更改,但通常是不必要的,因为Session具有一个被称为自动刷新的行为,我们稍后会说明。每当调用Session.commit()时,它也会刷新更改。

自动生成的主键属性

一旦行被插入,我们创建的两个 Python 对象处于一种称为持久性的状态,它们与它们所添加或加载的Session对象相关联,并具有许多其他行为,稍后将进行介绍。

INSERT 操作的另一个效果是 ORM 检索了每个新对象的新主键标识符;内部通常使用我们之前介绍的相同的CursorResult.inserted_primary_key访问器。squidwardkrabs对象现在具有这些新的主键标识符,并且我们可以通过访问id属性查看它们:

>>> squidward.id
4
>>> krabs.id
5

提示

为什么 ORM 在可以使用 executemany 时发出两个单独的 INSERT 语句?正如我们将在下一节中看到的那样,当刷新对象时,Session总是需要知道新插入对象的主键。如果使用了诸如 SQLite 的自动增量(其他示例包括 PostgreSQL IDENTITY 或 SERIAL,使用序列等)之类的功能,则CursorResult.inserted_primary_key功能通常要求每个 INSERT 逐行发出。如果我们事先提供了主键的值,ORM 将能够更好地优化操作。一些数据库后端,如 psycopg2,也可以一次插入多行,同时仍然能够检索主键值。

通过主键从标识映射获取对象

对象的主键标识对于Session来说非常重要,因为这些对象现在使用一种称为标识映射的特性与此标识在内存中连接起来。标识映射是一个在内存中的存储器,将当前加载在内存中的所有对象链接到它们的主键标识。我们可以通过使用Session.get()方法检索上述对象之一来观察到这一点,如果本地存在,则会从标识映射中返回一个条目,否则会发出一个 SELECT:

>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

关于标识映射的重要事情是,它在特定Session对象的范围内维护特定数据库标识的特定 Python 对象的唯一实例。我们可以观察到,some_squidward指的是先前的squidward相同对象

>>> some_squidward is squidward
True

标识映射是一个关键特性,它允许在事务中处理复杂的对象集合而不会使事情失去同步。

Committing

关于Session的工作还有很多要说的内容,这将在更进一步讨论。现在我们将提交事务,以便在查看更多 ORM 行为和特性之前构建对如何 SELECT 行的知识:

>>> session.commit()
COMMIT

上述操作将提交进行中的事务。我们处理过的对象仍然附加到Session上,这是它们保持的状态,直到关闭Session(在关闭会话中介绍)。

提示

值得注意的一点是,我们刚刚使用的对象上的属性已经过期,意味着当我们下次访问它们的任何属性时,Session将启动一个新的事务并重新加载它们的状态。这个选项有时会因为性能原因或者如果希望在关闭Session后继续使用对象(即已知的分离状态),而带来问题,因为它们将没有任何状态,并且将没有任何Session来加载该状态,导致“分离实例”错误。这种行为可以通过一个名为Session.expire_on_commit的参数来控制。更多信息请参阅关闭会话。

类的实例代表行

在前面的示例中,我们使用 Python 字典发出了一个 INSERT,以指示我们想要添加的数据,而使用 ORM 时,我们直接使用了我们定义的自定义 Python 类,在使用 ORM 声明式表单定义表元数据回到之前。在类级别上,UserAddress类用作定义相应数据库表应该是什么样子的地方。这些类还充当我们用于在事务内创建和操作行的可扩展数据对象。接下来,我们将创建两个User对象,每个对象都代表一个可能要 INSERT 的数据库行:

>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

我们能够使用映射列的名称作为构造函数中的关键字参数来构造这些对象。这是可能的,因为User类包含了一个由 ORM 映射提供的自动生成的__init__()构造函数,以便我们可以使用列名作为构造函数中的键来创建每个对象。

与我们在核心示例中的Insert类似,我们没有包含主键(即id列的条目),因为我们希望利用数据库的自动递增主键功能,本例中为 SQLite,ORM 也与之集成。如果我们查看上述对象的id属性的值,会显示为None

>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

None值由 SQLAlchemy 提供,表示该属性目前没有值。SQLAlchemy 映射的属性始终在 Python 中返回一个值,并且在处理尚未分配值的新对象时,不会引发AttributeError

目前,我们上面的两个对象被称为瞬态状态 - 它们与任何数据库状态都没有关联,尚未与可以为它们生成 INSERT 语句的Session对象关联。

将对象添加到会话

为了逐步说明添加过程,我们将创建一个不使用上下文管理器的Session(因此我们必须确保稍后关闭它!):

>>> session = Session(engine)

然后使用Session.add()方法将对象添加到Session中。调用此方法时,对象处于称为待定状态,尚未插入:

>>> session.add(squidward)
>>> session.add(krabs)

当我们有待定对象时,可以通过查看Session上的一个集合来查看这种状态,该集合称为Session.new:

>>> session.new
IdentitySet([User(id=None, name='squidward', fullname='Squidward Tentacles'), User(id=None, name='ehkrabs', fullname='Eugene H. Krabs')])

上述视图使用一个名为IdentitySet的集合,它本质上是一个 Python 集合,在所有情况下都使用对象标识哈希(即使用 Python 内置的id()函数,而不是 Python 的hash()函数)。

刷新

Session使用一种称为工作单元的模式。这通常意味着它逐个累积更改,但直到需要才实际将它们传达给数据库。这使其能够根据给定的一组待定更改更好地决定应该如何发出 SQL DML。当它向数据库发出 SQL 以推送当前一组更改时,该过程称为刷新

我们可以通过调用Session.flush()方法手动说明刷新过程:

>>> session.flush()
BEGIN  (implicit)
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  ('squidward',  'Squidward Tentacles')
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  ('ehkrabs',  'Eugene H. Krabs') 

首先我们观察到 Session 首次被调用以发出 SQL,因此它创建了一个新的事务并为两个对象发出了适当的 INSERT 语句。这个事务现在 保持开启,直到我们调用 Session.commit()Session.rollback()Session.close() 方法之一。

虽然 Session.flush() 可以用来手动推送待定更改到当前事务,但通常不需要,因为 Session 具有一种称为 自动刷新 的行为,稍后我们将说明。它还会在每次调用 Session.commit() 时刷新更改。

自动生成的主键属性

一旦行被插入,我们创建的两个 Python 对象处于所谓的 持久化 状态,它们与它们被添加或加载的 Session 对象相关联,并具有稍后将会介绍的许多其他行为。

INSERT 操作的另一个效果是 ORM 检索了每个新对象的新主键标识符;内部通常使用我们之前介绍的相同的 CursorResult.inserted_primary_key 访问器。squidwardkrabs 对象现在与这些新的主键标识符相关联,我们可以通过访问 id 属性查看它们:

>>> squidward.id
4
>>> krabs.id
5

提示

为什么 ORM 在可以使用 executemany 的情况下发出了两个单独的 INSERT 语句?正如我们将在下一节中看到的那样,当刷新对象时,Session始终需要知道新插入对象的主键。如果使用了诸如 SQLite 的自增等功能(其他示例包括 PostgreSQL 的 IDENTITY 或 SERIAL,使用序列等),则CursorResult.inserted_primary_key特性通常要求每个 INSERT 一次发出一行。如果我们提前为主键提供了值,ORM 将能够更好地优化操作。一些数据库后端,如 psycopg2,也可以一次插入多行,同时仍然能够检索主键值。

从标识映射获取主键的对象

对象的主键身份对于Session非常重要,因为现在使用称为标识映射的功能将对象与此标识在内存中连接起来。标识映射是一个在内存中的存储,它将当前加载在内存中的所有对象与它们的主键标识连接起来。我们可以通过使用Session.get()方法之一检索上述对象来观察到这一点,如果在本地存在,则会从标识映射返回一个条目,否则会发出一个 SELECT:

>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

关于标识映射的重要事情是,它在特定的Session对象的范围内维护了一个特定数据库标识的特定 Python 对象的唯一实例。我们可以观察到,some_squidward引用的是之前squidward同一对象

>>> some_squidward is squidward
True

标识映射是一个关键功能,允许在事务中操作复杂的对象集合而不会出现不同步的情况。

提交

关于Session如何工作还有很多要说的内容,这将在以后进一步讨论。目前,我们将提交事务,以便在检查更多 ORM 行为和特性之前积累关于如何 SELECT 行的知识:

>>> session.commit()
COMMIT

上述操作将提交正在进行的事务。我们处理过的对象仍然附加到Session,这是它们保持的状态,直到Session关闭(在关闭会话中介绍)。

提示

需要注意的重要事项是,我们刚刚处理过的对象上的属性已经过期,意味着,当我们下次访问它们的任何属性时,Session将启动一个新事务并重新加载它们的状态。这个选项有时会因为性能原因或者在关闭Session后希望使用对象(即分离状态)而带来问题,因为它们将不再具有任何状态,并且没有Session来加载该状态,导致“分离实例”错误。这种行为可以通过一个名为Session.expire_on_commit的参数来控制。更多信息请参考关闭会话。

使用工作单元模式更新 ORM 对象

在前面的章节使用 UPDATE 和 DELETE 语句中,我们介绍了代表 SQL UPDATE 语句的Update构造。在使用 ORM 时,有两种方式可以使用这个构造。主要方式是它会自动作为Session使用的工作单元过程的一部分发出,其中会针对具有更改的单个对象按照每个主键的方式发出 UPDATE 语句。

假设我们将用户名为sandyUser对象加载到一个事务中(同时展示Select.filter_by()方法以及Result.scalar_one()方法):

>>> sandy = session.execute(select(User).filter_by(name="sandy")).scalar_one()
BEGIN  (implicit)
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('sandy',) 

如前所述,Python 对象sandy充当数据库中行的代理,更具体地说是当前事务中具有主键标识2的数据库行:

>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')

如果我们更改此对象的属性,Session会跟踪此更改:

>>> sandy.fullname = "Sandy Squirrel"

对象出现在称为Session.dirty的集合中,表示对象是“脏”的:

>>> sandy in session.dirty
True

Session再次发出刷新时,将发出一个更新,将此值在数据库中更新。如前所述,在发出任何 SELECT 之前,刷新会自动发生,使用称为自动刷新的行为。我们可以直接查询该行的 User.fullname 列,我们将得到我们的更新值:

>>> sandy_fullname = session.execute(select(User.fullname).where(User.id == 2)).scalar_one()
UPDATE  user_account  SET  fullname=?  WHERE  user_account.id  =  ?
[...]  ('Sandy Squirrel',  2)
SELECT  user_account.fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (2,)
>>> print(sandy_fullname)
Sandy Squirrel

我们可以看到我们请求Session执行了一个单独的select()语句。但是,发出的 SQL 表明还发出了 UPDATE,这是刷新过程推出挂起更改。sandy Python 对象现在不再被视为脏:

>>> sandy in session.dirty
False

但请注意,我们仍然处于事务中,我们的更改尚未推送到数据库的永久存储中。由于 Sandy 的姓实际上是“Cheeks”而不是“Squirrel”,我们稍后会在回滚事务时修复此错误。但首先我们将进行更多的数据更改。

另见

刷新-详细介绍了刷新过程以及有关Session.autoflush设置的信息。

使用工作单元模式删除 ORM 对象

为了完善基本的持久性操作,可以通过使用Session.delete()方法在工作单元过程中标记要删除的单个 ORM 对象。让我们从数据库中加载 patrick

>>> patrick = session.get(User, 3)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,
user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (3,) 

如果我们标记patrick要删除,就像其他操作一样,直到刷新进行,实际上什么也不会发生:

>>> session.delete(patrick)

当前的 ORM 行为是,patrick 会留在Session中,直到刷新进行,正如之前提到的,如果我们发出查询:

>>> session.execute(select(User).where(User.name == "patrick")).first()
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,
address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (3,)
DELETE  FROM  user_account  WHERE  user_account.id  =  ?
[...]  (3,)
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('patrick',) 

上面,我们要求发出的 SELECT 之前是一个 DELETE,这表明了对 patrick 的待删除操作。还有一个针对 address 表的 SELECT,这是因为 ORM 在该表中查找可能与目标行相关的行;这种行为是作为 级联 行为的一部分,并且可以通过允许数据库自动处理 address 中的相关行来更高效地进行调整;delete 部分详细介绍了这一点。

另请参阅

delete - 描述了如何调整 Session.delete() 的行为,以便处理其他表中相关行的方式。

此外,正在删除的 patrick 对象实例不再被认为是 Session 中的持久对象,这可以通过包含检查来展示:

>>> patrick in session
False

然而,就像我们对 sandy 对象进行的 UPDATE 一样,我们在这里所做的每一个更改都仅限于正在进行的事务,如果我们不提交它,这些更改就不会变得永久。由于在当前情况下回滚事务更有趣,我们将在下一节中执行该操作。

批量/多行 INSERT、upsert、UPDATE 和 DELETE

此部分讨论的工作单元技术旨在将 dml 或 INSERT/UPDATE/DELETE 语句与 Python 对象机制集成,通常涉及复杂的相互关联对象图。一旦对象使用 Session.add() 添加到 Session 中,工作单元过程将自动代表我们发出 INSERT/UPDATE/DELETE,因为我们的对象属性被创建和修改。

然而,ORM Session 还具有处理命令的能力,使其能够直接发出 INSERT、UPDATE 和 DELETE 语句,而无需传递任何 ORM 持久化的对象,而是传递要 INSERT、UPDATE 或 upsert 或 WHERE 条件的值列表,以便一次匹配多行的 UPDATE 或 DELETE 语句可以被调用。当需要影响大量行而无需构造和操作映射对象时,此使用模式尤为重要,因为对于简单、性能密集的任务(如大型批量插入),构造和操作映射对象可能会很麻烦和不必要。

ORM Session的批量/多行功能直接使用了 insert()update()delete() 构造,并且它们的使用方式类似于它们在 SQLAlchemy Core 中的使用方式(首次在本教程中介绍了使用 INSERT 语句和使用 UPDATE 和 DELETE 语句)。当使用这些构造与 ORM Session 而不是普通的Connection时,它们的构建、执行和结果处理与 ORM 完全集成。

有关使用这些功能的背景和示例,请参见 ORM 启用的 INSERT、UPDATE 和 DELETE 语句部分,在 ORM 查询指南中。

另请参见

ORM 启用的 INSERT、UPDATE 和 DELETE 语句 - 在 ORM 查询指南中

回滚

Session有一个 Session.rollback() 方法,如预期的那样,在进行中的 SQL 连接上发出一个 ROLLBACK。然而,它也对当前与Session关联的对象产生影响,例如我们之前的示例中的 Python 对象sandy。虽然我们已经将sandy对象的.fullname更改为"Sandy Squirrel",但我们想要回滚此更改。调用Session.rollback()不仅会回滚事务,还会使当前与此Session关联的所有对象过期,这将导致它们在下次使用时自动刷新,这个过程称为惰性加载:

>>> session.rollback()
ROLLBACK

要更仔细地查看“到期”过程,我们可以观察到 Python 对象sandy在其 Python __dict__中没有剩余的状态,除了一个特殊的 SQLAlchemy 内部状态对象:

>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

这是“过期”状态;再次访问属性将自动开始一个新的事务,并使用当前数据库行刷新sandy

>>> sandy.fullname
BEGIN  (implicit)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,
user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (2,)
'Sandy Cheeks'

现在我们可以观察到__dict__中还填充了sandy对象的完整数据库行:

>>> sandy.__dict__  
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

对于已删除的对象,当我们之前注意到patrick不再在会话中时,该对象的标识也被恢复:

>>> patrick in session
True

当然,数据库数据也再次出现了:

>>> session.execute(select(User).where(User.name == "patrick")).scalar_one() is patrick
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('patrick',)
True

关闭会话

在上述部分中,我们在 Python 上下文管理器之外使用了一个Session对象,也就是说,我们没有使用with语句。虽然这样做没问题,但如果我们以这种方式操作,最好在完成后明确关闭Session

>>> session.close()
ROLLBACK 

关闭Session,也就是我们在上下文管理器中使用它时发生的事情,可以完成以下工作:

  • 它释放所有连接资源到连接池,取消(例如回滚)任何正在进行的事务。

    这意味着当我们使用会话执行一些只读任务然后关闭它时,我们不需要显式调用Session.rollback()来确保事务被回滚;连接池会处理这个。

  • Session中清除所有对象。

    这意味着我们为此Session加载的所有 Python 对象,如sandypatricksquidward,现在处于一种称为分离(detached)的状态。特别是,我们会注意到仍处于过期(expired)状态的对象,例如由于对Session.commit()的调用而导致的对象,现在已经不再可用,因为它们不包含当前行的状态,也不再与任何数据库事务相关联以进行刷新:

    # note that 'squidward.name' was just expired previously, so its value is unloaded
    >>> squidward.name
    Traceback (most recent call last):
      ...
    sqlalchemy.orm.exc.DetachedInstanceError: Instance <User at 0x...> is not bound to a Session; attribute refresh operation cannot proceed
    

    分离的对象可以使用Session.add()方法重新关联到相同或新的Session中,该方法将重新建立它们与特定数据库行的关系:

    >>> session.add(squidward)
    >>> squidward.name
    BEGIN  (implicit)
    SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,  user_account.fullname  AS  user_account_fullname
    FROM  user_account
    WHERE  user_account.id  =  ?
    [...]  (4,)
    'squidward'
    

    提示

    尽量避免在可能的情况下使用对象处于分离状态。当Session关闭时,同时清理所有先前附加对象的引用。对于需要分离对象的情况,通常是在 Web 应用程序中即时显示刚提交的对象,而Session在视图呈现之前关闭的情况下,将Session.expire_on_commit标志设置为False

处理 ORM 相关对象

原文:docs.sqlalchemy.org/en/20/tutorial/orm_related_objects.html

在本节中,我们将涵盖另一个重要的 ORM 概念,即 ORM 如何与引用其他对象的映射类交互。在 声明映射类 部分,映射类示例使用了一种称为 relationship() 的构造。此构造定义了两个不同映射类之间的链接,或者从一个映射类到它自身,后者称为自引用关系。

要描述 relationship() 的基本思想,首先我们将以简短形式回顾映射,省略 mapped_column() 映射和其他指令。

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = "user_account"

    # ... mapped_column() mappings

    addresses: Mapped[List["Address"]] = relationship(back_populates="user")

class Address(Base):
    __tablename__ = "address"

    # ... mapped_column() mappings

    user: Mapped["User"] = relationship(back_populates="addresses")

如上,User 类现在有一个属性 User.addresses,而 Address 类有一个属性 Address.userrelationship() 构造与 Mapped 构造一起指示类型行为,将用于检查与 UserAddress 类映射到的 Table 对象之间的表关系。由于代表 address 表的 Table 对象具有指向 user_account 表的 ForeignKeyConstraintrelationship() 可以明确确定从 User 类到 Address 类的 一对多 关系,沿着 User.addresses 关系;user_account 表中的一个特定行可能被 address 表中的多行引用。

所有一对多关系自然对应于另一个方向的多对一关系,在本例中由Address.user指出。如上所示在两个relationship()对象上配置的relationship.back_populates参数,建立了这两个relationship()构造应被视为彼此补充;我们将在下一节中看到这是如何运作的。

持久化和加载关系

我们可以首先说明relationship()对对象实例做了什么。如果我们创建一个新的User对象,我们可以注意到当我们访问.addresses元素时有一个 Python 列表:

>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]

此对象是 Python list的 SQLAlchemy 特定版本,具有跟踪和响应对其进行的更改的能力。即使我们从未将其分配给对象,当我们访问属性时,集合也会自动出现。这类似于在使用 ORM 工作单元模式插入行中观察到的行为,在那里我们观察到,我们没有明确为其分配值的基于列的属性也会自动显示为None,而不是像 Python 通常行为一样引发AttributeError

由于u1对象仍然是瞬态,我们从u1.addresses获取的list尚未发生变异(即未被追加或扩展),因此它实际上还没有与对象关联,但随着我们对其进行更改,它将成为User对象状态的一部分。

该集合专用于Address类,这是唯一可以在其中持久化的 Python 对象类型。我们可以使用list.append()方法添加一个Address对象:

>>> a1 = Address(email_address="pearl.krabs@gmail.com")
>>> u1.addresses.append(a1)

此时,u1.addresses集合如预期中包含新的Address对象:

>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]

当我们将Address对象与u1实例的User.addresses集合关联时,还发生了另一个行为,即User.addresses关系将自动与Address.user关系同步,这样我们不仅可以从User对象导航到Address对象,还可以从Address对象导航回“父”User对象:

>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')

此同步是由我们在两个 relationship() 对象之间使用的 relationship.back_populates 参数导致的。此参数命名了另一个应该发生补充属性赋值/列表变异的 relationship() 。在另一个方向上同样有效,即如果我们创建另一个 Address 对象并将其分配给其 Address.user 属性,那么该 Address 将成为该 User 对象上的 User.addresses 集合的一部分:

>>> a2 = Address(email_address="pearl@aol.com", user=u1)
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')]

我们实际上在 Address 构造函数中使用了 user 参数作为关键字参数,它像在 Address 类上声明的任何其他映射属性一样被接受。这相当于在事后分配了 Address.user 属性:

# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1

将对象级联到会话中

我们现在有一个 User 和两个 Address 对象,它们在内存中以双向结构关联,但正如之前在 使用 ORM 单元工作模式插入行 中所指出的,这些对象被认为处于 瞬时态 ,直到它们与一个 Session 对象关联。

我们继续使用正在进行中的 Session ,注意当我们对主要的 User 对象应用 Session.add() 方法时,相关的 Address 对象也被添加到同一个 Session 中:

>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True

上述行为,Session 接收了一个 User 对象,并沿着 User.addresses 关系找到了相关的 Address 对象,被称为 保存-更新级联,在 ORM 参考文档的 级联 中详细讨论。

这三个对象现在处于 pending 状态;这意味着它们准备好被用于 INSERT 操作,但这还没有进行;所有三个对象都还没有分配主键,并且a1a2对象还有一个名为user_id的属性,它指向具有引用user_account.id列的Column,这些也都是None,因为这些对象还没有与真实的数据库行关联:

>>> print(u1.id)
None
>>> print(a1.user_id)
None

正是在这个阶段,我们可以看到工作单元过程提供的非常大的实用性;回想在 INSERT 通常会自动生成“values”子句一节中,使用一些复杂的语法将行插入到user_accountaddress表中,以便自动将address.user_id列与user_account行的列关联起来。此外,我们需要先为user_account行发出 INSERT,然后再为address行发出 INSERT,因为address行依赖于其父行user_accountuser_id列的值。

当使用Session时,所有这些繁琐的工作都会为我们处理,即使是最顽固的 SQL 纯粹主义者也可以从 INSERT、UPDATE 和 DELETE 语句的自动化中受益。当我们调用Session.commit()提交事务时,所有步骤按正确顺序调用,而且user_account行的新生成主键也会适当地应用到address.user_id列上:

>>> session.commit()
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)
[...]  ('pkrabs',  'Pearl Krabs')
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  ('pearl.krabs@gmail.com',  6)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  ('pearl@aol.com',  6)
COMMIT 
```  ## 加载关系

在上一步中,我们调用了`Session.commit()`,这会为事务发出一个 COMMIT,然后根据`Session.commit.expire_on_commit`使所有对象过期,以便它们在下一个事务中刷新。

当我们下次访问这些对象的属性时,我们会看到为行的主要属性发出的 SELECT,比如当我们查看`u1`对象的新生成的主键时:

```py
>>> u1.id
BEGIN  (implicit)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,
user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (6,)
6

u1 User对象现在有一个持久化集合User.addresses,我们也可以访问它。由于这个集合包含了address表中的一组额外行,当我们再次访问这个集合时,我们会再次看到一个延迟加载以检索对象:

>>> u1.addresses
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,
address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (6,)
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

SQLAlchemy ORM 中的集合和相关属性在内存中是持久的;一旦集合或属性被填充,SQL 就不再发出,直到该集合或属性被过期。我们可以再次访问u1.addresses,以及添加或删除项目,并且这不会产生任何新的 SQL 调用:

>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

虽然懒加载所发出的加载请求如果我们不采取明确的优化步骤就很容易变得昂贵,但至少懒加载的网络相当优化,不会执行冗余的工作;由于 u1.addresses 集合被刷新,根据身份映射,这些实际上是我们已经处理过的a1a2对象中的相同的Address实例,因此我们已经完成了加载这个特定对象图中的所有属性:

>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')

关系如何加载或不加载的问题是一个独立的主题。稍后在本节的加载策略中对这些概念进行了一些补充介绍。 ## 在查询中使用关系

前一节介绍了当使用映射类的实例relationship()构造的行为,上文介绍了UserAddress类的u1a1a2实例。在本节中,我们介绍了当应用于映射类的类级行为时,relationship()的行为,它在多个方面帮助自动构建 SQL 查询。

使用关系进行连接

显式 FROM 子句和 JOINs 和设置 ON 子句章节介绍了使用Select.join()Select.join_from()方法来组合 SQL JOIN 子句。为了描述如何在表之间进行连接,这些方法要么根据表元数据结构中存在的单个明确的ForeignKeyConstraint对象推断出 ON 子句,该对象链接了这两个表,要么我们可以提供一个明确的 SQL 表达式构造,指示特定的 ON 子句。

当使用 ORM 实体时,还有一种额外的机制可用于帮助我们设置连接的 ON 子句,这就是利用我们在用户映射中设置的 relationship() 对象,就像在 声明映射类 中演示的那样。相应于 relationship() 的类绑定属性可以作为 单个参数 传递给 Select.join(),它既用于指示连接的右侧,又一次性指示 ON 子句:

>>> print(select(Address.email_address).select_from(User).join(User.addresses))
SELECT  address.email_address
FROM  user_account  JOIN  address  ON  user_account.id  =  address.user_id 

映射上的 ORM relationship() 的存在,如果我们没有指定 ON 子句,将不会被 Select.join()Select.join_from() 用于推断 ON 子句。这意味着,如果我们从 User 连接到 Address 而没有 ON 子句,它会工作是因为两个映射的 Table 对象之间的 ForeignKeyConstraint,而不是 UserAddress 类上的 relationship() 对象:

>>> print(select(Address.email_address).join_from(User, Address))
SELECT  address.email_address
FROM  user_account  JOIN  address  ON  user_account.id  =  address.user_id 

请参阅 连接 在 ORM 查询指南 中,了解如何使用 Select.join()Select.join_from()relationship() 构造的更多示例。

请参见

连接 在 ORM 查询指南 ### 关系 WHERE 运算符

relationship() 还配备了一些额外的 SQL 生成辅助工具,当构建语句的 WHERE 子句时通常很有用。请参阅 关系 WHERE 运算符 在 ORM 查询指南 中的部分。

请参见

关系 WHERE 运算符在 ORM 查询指南中 ## 加载策略

在加载关系部分,我们介绍了这样一个概念,当我们使用映射对象的实例时,访问使用relationship()映射的属性时,在默认情况下,如果集合未填充,则会发出延迟加载以加载应该存在于此集合中的对象。

延迟加载是最著名的 ORM 模式之一,也是最具争议的模式之一。当内存中有几十个 ORM 对象分别引用少量未加载的属性时,对这些对象的常规操作可能会产生许多额外的查询,这些查询可能会累积(也称为 N 加一问题),更糟糕的是它们是隐式发出的。这些隐式查询可能不会被注意到,在数据库事务不再可用时尝试执行它们时可能会导致错误,或者在使用诸如 asyncio 之类的替代并发模式时,它们实际上根本不起作用。

与此同时,当与正在使用的并发方法兼容且没有引起问题时,延迟加载是一种非常流行和有用的模式。出于这些原因,SQLAlchemy 的 ORM 非常重视能够控制和优化这种加载行为。

首先,有效使用 ORM 延迟加载的第一步是测试应用程序,打开 SQL 回显,并观察生成的 SQL 语句。如果看起来有很多冗余的 SELECT 语句,看起来它们可以更有效地合并为一个,如果对象在已经分离的Session中不适当地发生加载,那就是使用加载策略的时候。

加载策略表示为可以使用Select.options()方法与 SELECT 语句关联的对象,例如:

for user_obj in session.execute(
    select(User).options(selectinload(User.addresses))
).scalars():
    user_obj.addresses  # access addresses collection already loaded

它们也可以被配置为relationship()的默认值,使用relationship.lazy选项,例如:

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = "user_account"

    addresses: Mapped[List["Address"]] = relationship(
        back_populates="user", lazy="selectin"
    )

每个加载器策略对象都会向语句中添加某种信息,该信息将在以后由Session在决定各种属性在访问时应如何加载和/或行为时使用。

下面的部分将介绍一些最常用的加载器策略。

参见

关系加载技术中的两个部分:

  • 在映射时配置加载器策略 - 配置在relationship()上的策略的详细信息

  • 使用加载器选项进行关系加载 - 使用查询时加载策略的详细信息

Selectin Load

在现代 SQLAlchemy 中最有用的加载器是selectinload()加载器选项。该选项解决了最常见形式的“N 加一”问题,即一组对象引用相关集合。selectinload()将确保立即使用单个查询加载整个系列对象的特定集合。它使用一种 SELECT 形式,在大多数情况下可以针对相关表单独发出,而不需要引入 JOIN 或子查询,并且仅查询那些集合尚未加载的父对象。下面我们通过加载所有User对象及其所有相关的Address对象来说明selectinload();虽然我们只调用了一次Session.execute(),给定一个select()构造,但在访问数据库时,实际上发出了两个 SELECT 语句,第二个语句是用于获取相关的Address对象:

>>> from sqlalchemy.orm import selectinload
>>> stmt = select(User).options(selectinload(User.addresses)).order_by(User.id)
>>> for row in session.execute(stmt):
...     print(
...         f"{row.User.name}  ({', '.join(a.email_address for a in row.User.addresses)})"
...     )
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account  ORDER  BY  user_account.id
[...]  ()
SELECT  address.user_id  AS  address_user_id,  address.id  AS  address_id,
address.email_address  AS  address_email_address
FROM  address
WHERE  address.user_id  IN  (?,  ?,  ?,  ?,  ?,  ?)
[...]  (1,  2,  3,  4,  5,  6)
spongebob  (spongebob@sqlalchemy.org)
sandy  (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick  ()
squidward  ()
ehkrabs  ()
pkrabs  (pearl.krabs@gmail.com, pearl@aol.com)

参见

选择 IN 加载 - 在关系加载技术中

Joined Load

joinedload()预加载策略是 SQLAlchemy 中最古老的预加载器,它通过在传递给数据库的 SELECT 语句中添加一个 JOIN(根据选项可能是外连接或内连接)来增强,然后可以加载相关对象。

joinedload()策略最适合加载相关的多对一对象,因为这只需要向主实体行添加额外的列,在任何情况下都会获取这些列。为了提高效率,它还接受一个选项joinedload.innerjoin,这样在下面这种情况下可以使用内连接而不是外连接,我们知道所有的Address对象都有一个关联的User

>>> from sqlalchemy.orm import joinedload
>>> stmt = (
...     select(Address)
...     .options(joinedload(Address.user, innerjoin=True))
...     .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
...     print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT  address.id,  address.email_address,  address.user_id,  user_account_1.id  AS  id_1,
user_account_1.name,  user_account_1.fullname
FROM  address
JOIN  user_account  AS  user_account_1  ON  user_account_1.id  =  address.user_id
ORDER  BY  address.id
[...]  ()
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs

joinedload()也适用于集合,意味着一对多关系,但它会以递归方式将每个相关项乘以主行,从而增加通过结果集发送的数据量,对于嵌套集合和/或较大集合,这会使数据量成倍增长,因此应该根据具体情况评估其与其他选项(例如selectinload())的使用。

需要注意的是,封闭Select语句的 WHERE 和 ORDER BY 条件不会针对 joinedload()生成的表。上面的例子中,可以看到 SQL 中对user_account表应用了一个匿名别名,以便在查询中无法直接寻址。这个概念在加入式预加载的禅意一节中有更详细的讨论。

提示

需要注意的是,多对一的预加载通常是不必要的,因为“N 加一”问题在常见情况下要少得多。当许多对象都引用相同的相关对象时,例如每个都引用相同User的许多Address对象时,SQL 将仅对该User对象发出一次,使用普通的惰性加载。惰性加载例程将在当前Session中尽可能地通过主键查找相关对象,而不在可能时发出任何 SQL。

另请参阅

加入式预加载 - 在关系加载技术中

明确的连接 + 预加载

如果我们在连接到 user_account 表时加载 Address 行,使用诸如 Select.join() 之类的方法来渲染 JOIN,我们也可以利用该 JOIN 来急切地加载每个返回的 Address 对象的 Address.user 属性的内容。这本质上就是我们正在使用“连接的急切加载”,但是自己渲染 JOIN。这个常见的用例是通过使用 contains_eager() 选项实现的。该选项与 joinedload() 非常相似,只是它假设我们已经自己设置了 JOIN,并且它仅指示应该将 COLUMNS 子句中的附加列加载到每个返回对象的相关属性中,例如:

>>> from sqlalchemy.orm import contains_eager
>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "pkrabs")
...     .options(contains_eager(Address.user))
...     .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
...     print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT  user_account.id,  user_account.name,  user_account.fullname,
address.id  AS  id_1,  address.email_address,  address.user_id
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
WHERE  user_account.name  =  ?  ORDER  BY  address.id
[...]  ('pkrabs',)
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs

上面,我们同时对 user_account.name 进行了筛选,并且将 user_account 中的行加载到返回的行的 Address.user 属性中。如果我们分别应用了 joinedload() ,我们将会得到一个不必要两次连接的 SQL 查询:

>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "pkrabs")
...     .options(joinedload(Address.user))
...     .order_by(Address.id)
... )
>>> print(stmt)  # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
SELECT  address.id,  address.email_address,  address.user_id,
user_account_1.id  AS  id_1,  user_account_1.name,  user_account_1.fullname
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
LEFT  OUTER  JOIN  user_account  AS  user_account_1  ON  user_account_1.id  =  address.user_id
WHERE  user_account.name  =  :name_1  ORDER  BY  address.id 

另请参阅

关系加载技术中的两个部分:

  • 连接急切加载的禅意 - 详细描述了上述问题

  • 将显式连接/语句路由到急切加载的集合 - 使用 contains_eager()

Raiseload

值得一提的另一个加载器策略是 raiseload() 。此选项用于通过导致通常将是延迟加载的操作引发错误来完全阻止应用程序遇到 N 加一 问题。它有两个变体,通过 raiseload.sql_only 选项进行控制,以阻止需要 SQL 的延迟加载,与所有“加载”操作,包括仅需要查询当前 Session 的那些操作。

使用 raiseload() 的一种方法是在 relationship() 上配置它,通过将 relationship.lazy 设置为值 "raise_on_sql",这样对于特定映射,某个关系将永远不会尝试发出 SQL:

>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import relationship

>>> class User(Base):
...     __tablename__ = "user_account"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     addresses: Mapped[List["Address"]] = relationship(
...         back_populates="user", lazy="raise_on_sql"
...     )

>>> class Address(Base):
...     __tablename__ = "address"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...     user: Mapped["User"] = relationship(back_populates="addresses", lazy="raise_on_sql")

使用这样的映射,应用程序被阻止了懒加载,表明特定查询需要指定一个加载策略:

>>> u1 = session.execute(select(User)).scalars().first()
SELECT  user_account.id  FROM  user_account
[...]  ()
>>> u1.addresses
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'

异常将指示应该预先加载此集合:

>>> u1 = (
...     session.execute(select(User).options(selectinload(User.addresses)))
...     .scalars()
...     .first()
... )
SELECT  user_account.id
FROM  user_account
[...]  ()
SELECT  address.user_id  AS  address_user_id,  address.id  AS  address_id
FROM  address
WHERE  address.user_id  IN  (?,  ?,  ?,  ?,  ?,  ?)
[...]  (1,  2,  3,  4,  5,  6) 

lazy="raise_on_sql" 选项也会对多对一关系进行智能处理;上面,如果一个 Address 对象的 Address.user 属性未加载,但是该 User 对象在同一个 Session 中本地存在,那么“raiseload”策略将不会引发错误。

另请参阅

使用 raiseload 阻止不必要的懒加载 - 在关系加载技术中

持久化和加载关系

我们可以先说明 relationship() 对象实例的作用。如果我们创建一个新的 User 对象,我们可以注意到当我们访问 .addresses 元素时会有一个 Python 列表:

>>> u1 = User(name="pkrabs", fullname="Pearl Krabs")
>>> u1.addresses
[]

此对象是 Python list 的 SQLAlchemy 特定版本,具有跟踪和响应对其进行的更改的能力。当我们访问属性时,集合也会自动出现,即使我们从未将其分配给对象。这类似于在 使用 ORM 工作单元模式插入行 中注意到的行为,即我们没有明确为其分配值的基于列的属性也会自动显示为 None,而不是像 Python 的通常行为那样引发 AttributeError

由于 u1 对象仍然是 瞬态,并且我们从 u1.addresses 得到的 list 尚未被改变(即未被添加或扩展),因此实际上尚未与对象关联,但是当我们对其进行更改时,它将成为 User 对象状态的一部分。

该集合专用于 Address 类,这是唯一可以在其中持久化的 Python 对象类型。使用 list.append() 方法,我们可以添加一个 Address 对象:

>>> a1 = Address(email_address="pearl.krabs@gmail.com")
>>> u1.addresses.append(a1)

此时,u1.addresses 集合按预期包含了新的 Address 对象:

>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]

当我们将Address对象与u1实例的User.addresses集合关联起来时,还发生了另一个行为,即User.addresses关系与Address.user关系同步,这样我们不仅可以从User对象导航到Address对象,还可以从Address对象导航回“父”User对象:

>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')

这种同步发生是因为我们在两个relationship()对象之间使用了relationship.back_populates参数。该参数命名了另一个应进行互补属性赋值/列表变异的relationship()。在另一个方向上同样有效,即如果我们创建另一个Address对象并将其分配给其Address.user属性,该Address将成为User对象上的User.addresses集合的一部分:

>>> a2 = Address(email_address="pearl@aol.com", user=u1)
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com'), Address(id=None, email_address='pearl@aol.com')]

我们实际上在Address构造函数中使用了user参数作为关键字参数,这与在Address类上声明的任何其他映射属性一样被接受。这相当于事后对Address.user属性进行赋值:

# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1

将对象级联到会话中

现在我们有一个User和两个Address对象,在内存中以双向结构关联,但如前所述,在使用 ORM 工作单元模式插入行中,这些对象被称为处于瞬态状态,直到它们与一个Session对象关联为止。

我们利用的是仍在进行中的Session,请注意,当我们对主User对象应用Session.add()方法时,相关的Address对象也会被添加到同一个Session中:

>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True

上述行为,即Session接收到一个User对象,并沿着User.addresses关系定位相关的Address对象的行为,被称为保存更新级联,并在 ORM 参考文档中详细讨论,链接地址为 Cascades。

这三个对象现在处于 挂起 状态;这意味着它们已经准备好成为 INSERT 操作的对象,但这还没有进行;所有三个对象目前还没有分配主键,并且此外,a1a2 对象具有一个名为 user_id 的属性,该属性指向具有引用 user_account.id 列的 Column,这些属性也是 None,因为这些对象尚未与真实的数据库行关联:

>>> print(u1.id)
None
>>> print(a1.user_id)
None

此时,我们可以看到工作单元流程提供的非常大的实用性;回想一下在 INSERT 通常会自动生成“values”子句 中,行是如何插入到 user_accountaddress 表中的,使用一些复杂的语法来自动将 address.user_id 列与 user_account 表中的列关联起来。此外,我们必须首先为 user_account 表中的行发出 INSERT,然后是 address 表中的行,因为 address 中的行依赖于其在 user_account 表中的父行,以获取其 user_id 列中的值。

使用 Session 时,所有这些烦琐工作都由我们处理,即使是最铁杆的 SQL 纯粹主义者也可以从 INSERT、UPDATE 和 DELETE 语句的自动化中受益。当我们调用 Session.commit() 时,所有步骤都按正确的顺序执行,并且还会将 user_account 行的新生成的主键适当地应用到 address.user_id 列中:

>>> session.commit()
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)
[...]  ('pkrabs',  'Pearl Krabs')
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  ('pearl.krabs@gmail.com',  6)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  ('pearl@aol.com',  6)
COMMIT 
```  ### 将对象级联到会话中

现在,我们在内存中有一个双向结构的 `User` 对象和两个 `Address` 对象,但正如之前在 使用 ORM 工作单元模式插入行 中所述,这些对象被认为处于 瞬时 状态,直到它们与一个 `Session` 对象关联为止。

我们利用的是仍在进行中的 `Session`,请注意,当我们将 `Session.add()` 方法应用于主 `User` 对象时,相关的 `Address` 对象也会被添加到同一个 `Session` 中:

```py
>>> session.add(u1)
>>> u1 in session
True
>>> a1 in session
True
>>> a2 in session
True

上述行为,其中Session接收到一个 User 对象,并沿着 User.addresses 关系跟踪以找到相关的 Address 对象,被称为save-update cascade,并且在 ORM 参考文档的 Cascades 中有详细讨论。

这三个对象现在处于 pending 状态;这意味着它们已准备好成为 INSERT 操作的主体,但还没有进行;这三个对象都还没有分配主键,并且此外,a1a2 对象具有一个名为 user_id 的属性,它指向具有引用 user_account.id 列的Column;由于这些对象尚未与真实的数据库行关联,因此这些值也都是 None

>>> print(u1.id)
None
>>> print(a1.user_id)
None

此时,我们可以看到工作单元流程提供的非常大的实用性;回想一下,在 INSERT 通常自动生成“values”子句一节中,我们使用一些复杂的语法将行插入到 user_accountaddress 表中,以便自动将 address.user_id 列与 user_account 行的列关联起来。此外,必须先为 user_account 行发出 INSERT,然后才能为 address 的行发出 INSERT,因为 address 中的行依赖于其父行 user_account 以在其 user_id 列中获得值。

当使用Session时,所有这些繁琐的工作都由我们处理,即使是最铁杆的 SQL 纯粹主义者也可以从 INSERT、UPDATE 和 DELETE 语句的自动化中受益。当我们调用Session.commit()提交事务时,所有步骤都按正确的顺序执行,并且新生成的 user_account 行的主键还会适当地应用到 address.user_id 列上:

>>> session.commit()
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)
[...]  ('pkrabs',  'Pearl Krabs')
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...  (insertmanyvalues)  1/2  (ordered;  batch  not  supported)]  ('pearl.krabs@gmail.com',  6)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[insertmanyvalues  2/2  (ordered;  batch  not  supported)]  ('pearl@aol.com',  6)
COMMIT 

加载关系

在最后一步中,我们调用了Session.commit(),它发出了一个 COMMIT 以提交事务,然后根据Session.commit.expire_on_commit将所有对象过期,以便它们为下一个事务刷新。

当我们下次访问这些对象的属性时,我们将看到为行的主要属性发出的 SELECT,例如当我们查看 u1 对象的新生成的主键时:

>>> u1.id
BEGIN  (implicit)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,
user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (6,)
6

现在 u1 User 对象具有一个持久集合 User.addresses,我们也可以访问它。由于此集合包含来自 address 表的一组额外行,因此当我们再次访问此集合时,我们会再次看到一个懒加载以检索对象:

>>> u1.addresses
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,
address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (6,)
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

SQLAlchemy ORM 中的集合和相关属性是在内存中持久存在的;一旦集合或属性被填充,SQL 就不再生成,直到该集合或属性被过期。我们可以再次访问 u1.addresses,并添加或删除项目,这不会产生任何新的 SQL 调用:

>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

如果我们不采取显式步骤来优化懒加载,懒加载引发的加载可能会很快变得昂贵,但至少懒加载的网络相对来说已经相当优化,不会执行冗余工作;因为 u1.addresses 集合已经刷新,根据标识映射,这些实际上是我们已经处理过的 a1a2 对象的同一 Address 实例,所以我们已经完成了加载此特定对象图中的所有属性:

>>> a1
Address(id=4, email_address='pearl.krabs@gmail.com')
>>> a2
Address(id=5, email_address='pearl@aol.com')

关系如何加载或不加载是一个独立的主题。稍后在本节的 加载器策略 中会对这些概念进行一些额外的介绍。

在查询中使用关系

前一节介绍了当与映射类的实例一起使用时 relationship() 构造的行为,上面是 UserAddress 类的 u1a1a2 实例。在本节中,我们将介绍当应用于映射类的类级行为relationship() 的行为,在这里,它以几种方式帮助自动构建 SQL 查询。

使用关系进行连接

显式的 FROM 子句和 JOINs 和 设置 ON 子句 章节介绍了使用 Select.join()Select.join_from() 方法来组合 SQL JOIN 子句。为了描述如何在表之间进行连接,这些方法要么**根据表元数据结构中链接两个表的单个明确的 ForeignKeyConstraint 对象推断出 ON 子句,要么我们可以提供一个明确的 SQL 表达式构造,指示特定的 ON 子句。

在使用 ORM 实体时,有一种额外的机制可帮助我们设置连接的 ON 子句,那就是利用我们在用户映射中设置的relationship()对象,就像在声明映射类中所演示的那样。相应于relationship()的类绑定属性可以作为单个参数传递给Select.join(),在这里它同时用于指示连接的右侧以及 ON 子句:

>>> print(select(Address.email_address).select_from(User).join(User.addresses))
SELECT  address.email_address
FROM  user_account  JOIN  address  ON  user_account.id  =  address.user_id 

如果我们没有指定 ON 子句,则映射上的 ORM relationship()Select.join()Select.join_from() 的存在不会用于推断 ON 子句。这意味着,如果我们从 User 连接到 Address 而没有 ON 子句,这是因为两个映射的 Table 对象之间的 ForeignKeyConstraint,而不是由于 UserAddress 类上的 relationship() 对象:

>>> print(select(Address.email_address).join_from(User, Address))
SELECT  address.email_address
FROM  user_account  JOIN  address  ON  user_account.id  =  address.user_id 

请参阅 ORM 查询指南中的连接一节,了解如何使用 Select.join()Select.join_from() 以及 relationship() 构造的更多示例。

另请参阅

ORM 查询指南中的连接 ### Relationship WHERE 运算符

还有一些额外的 SQL 生成辅助程序,随着 relationship() 一起提供,当构建语句的 WHERE 子句时通常很有用。请参阅 ORM 查询指南中的 Relationship WHERE 运算符一节。

另请参阅

ORM 查询指南中的关系 WHERE 运算符 ### 使用关系进行连接 在 ORM 查询指南

明确的 FROM 子句和 JOIN 和设置 ON 子句部分介绍了使用Select.join()Select.join_from()方法组成 SQL JOIN 子句的用法。为了描述如何在表之间进行连接,这些方法根据表元数据结构中链接两个表的单一明确ForeignKeyConstraint对象的存在推断 ON 子句,或者我们可以提供一个明确的 SQL 表达式构造来指示特定的 ON 子句。

在使用 ORM 实体时,有一种额外的机制可帮助我们设置连接的 ON 子句,即利用我们在用户映射中设置的relationship()对象,就像在声明映射类中所演示的那样。相应于relationship()的类绑定属性可以作为单个参数传递给Select.join(),在这里它既用于指示连接的右侧,又用于一次性指示 ON 子句:

>>> print(select(Address.email_address).select_from(User).join(User.addresses))
SELECT  address.email_address
FROM  user_account  JOIN  address  ON  user_account.id  =  address.user_id 

如果我们不指定,映射中的 ORM relationship()的存在不会被Select.join()Select.join_from()用于推断 ON 子句。这意味着,如果我们从 UserAddress 进行连接而没有 ON 子句,这是因为两个映射的 Table 对象之间的 ForeignKeyConstraint,而不是 UserAddress 类上的 relationship() 对象:

>>> print(select(Address.email_address).join_from(User, Address))
SELECT  address.email_address
FROM  user_account  JOIN  address  ON  user_account.id  =  address.user_id 

在 ORM 查询指南中查看连接(Joins)部分,了解如何使用Select.join()Select.join_from()以及relationship()构造的更多示例。

另请参阅

ORM 查询指南中的连接(Joins)

关系 WHERE 运算符

在构建语句的 WHERE 子句时,relationship()还附带了一些其他类型的 SQL 生成助手,通常在构建过程中非常有用。请查看 ORM 查询指南中的关系 WHERE 运算符部分。

另请参阅

在 ORM 查询指南中的关系 WHERE 运算符部分

加载策略

在加载关系部分,我们介绍了一个概念,即当我们处理映射对象的实例时,默认情况下访问使用relationship()映射的属性时,如果集合未填充,则会发出惰性加载以加载应该存在于此集合中的对象。

懒加载是最著名的 ORM 模式之一,也是最具争议的模式之一。当内存中有几十个 ORM 对象各自引用了少量未加载的属性时,对这些对象的常规操作可能会产生许多额外的查询,这些查询可能会积累起来(也被称为 N 加一问题),更糟糕的是它们是隐式生成的。这些隐式查询可能不会被注意到,在没有数据库事务可用时尝试使用它们时可能会导致错误,或者当使用诸如 asyncio 等替代并发模式时,它们实际上根本不起作用。

与此同时,当它与正在使用的并发方法兼容并且没有引起问题时,懒加载是一种非常受欢迎和有用的模式。因此,SQLAlchemy 的 ORM 非常强调能够控制和优化此加载行为。

最重要的是,有效使用 ORM 懒加载的第一步是测试应用程序,打开 SQL 回显,并观察发出的 SQL 语句。如果看起来有大量的冗余的 SELECT 语句,看起来很像它们可以更有效地合并为一个,如果发生了适用于已从其 Session 中分离的对象的不适当的加载,那么就要考虑使用加载器策略

加载器策略表示为对象,可以使用 Select.options() 方法将其与 SELECT 语句关联,例如:

for user_obj in session.execute(
    select(User).options(selectinload(User.addresses))
).scalars():
    user_obj.addresses  # access addresses collection already loaded

也可以将其配置为 relationship() 的默认值,使用 relationship.lazy 选项,例如:

from sqlalchemy.orm import Mapped
from sqlalchemy.orm import relationship

class User(Base):
    __tablename__ = "user_account"

    addresses: Mapped[List["Address"]] = relationship(
        back_populates="user", lazy="selectin"
    )

每个加载器策略对象都会向语句添加某种信息,该信息稍后将由 Session 在决定在访问属性时应如何加载和/或行为时使用。

下面的章节将介绍一些最常用的加载器策略。

另请参阅

关系加载技术 中的两个部分:

  • 在映射时配置加载器策略 - 详细介绍了在 relationship() 上配置策略的方法。

  • 使用加载器选项进行关系加载 - 详细介绍了使用查询时加载策略的方法。

Selectin Load

在现代 SQLAlchemy 中最有用的加载器是 selectinload() 加载器选项。该选项解决了“N plus one”问题的最常见形式,即一组对象引用相关集合。selectinload() 将确保通过单个查询一次性加载一系列对象的特定集合。它使用的 SELECT 形式在大多数情况下可以只针对相关表发出,而不需要引入 JOIN 或子查询,并且仅查询那些尚未加载集合的父对象。下面我们通过加载所有 User 对象及其所有相关的 Address 对象来说明 selectinload();虽然我们只调用一次 Session.execute(),但在访问数据库时实际上发出了两个 SELECT 语句,第二个语句用于获取相关的 Address 对象:

>>> from sqlalchemy.orm import selectinload
>>> stmt = select(User).options(selectinload(User.addresses)).order_by(User.id)
>>> for row in session.execute(stmt):
...     print(
...         f"{row.User.name}  ({', '.join(a.email_address for a in row.User.addresses)})"
...     )
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account  ORDER  BY  user_account.id
[...]  ()
SELECT  address.user_id  AS  address_user_id,  address.id  AS  address_id,
address.email_address  AS  address_email_address
FROM  address
WHERE  address.user_id  IN  (?,  ?,  ?,  ?,  ?,  ?)
[...]  (1,  2,  3,  4,  5,  6)
spongebob  (spongebob@sqlalchemy.org)
sandy  (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick  ()
squidward  ()
ehkrabs  ()
pkrabs  (pearl.krabs@gmail.com, pearl@aol.com)

另请参阅

选择 IN 加载 - 在关系加载技术中

联合加载

joinedload() 立即加载策略是 SQLAlchemy 中最古老的立即加载器,它通过将传递给数据库的 SELECT 语句与 JOIN(取决于选项可能是外连接或内连接)相结合,从而可以加载相关对象。

joinedload() 策略最适合于加载相关的多对一对象,因为这仅需要将额外的列添加到主实体行中,而这些列无论如何都会被获取。为了提高效率,它还接受一个选项 joinedload.innerjoin,以便在我们知道所有 Address 对象都有关联的 User 的情况下使用内连接而不是外连接:

>>> from sqlalchemy.orm import joinedload
>>> stmt = (
...     select(Address)
...     .options(joinedload(Address.user, innerjoin=True))
...     .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
...     print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT  address.id,  address.email_address,  address.user_id,  user_account_1.id  AS  id_1,
user_account_1.name,  user_account_1.fullname
FROM  address
JOIN  user_account  AS  user_account_1  ON  user_account_1.id  =  address.user_id
ORDER  BY  address.id
[...]  ()
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs

joinedload() 也适用于集合,即一对多关系,但它会以递归方式将主要行乘以相关项目,从而使结果集发送的数据量呈数量级增长,对于嵌套集合和/或较大集合,因此应该根据具体情况评估其与其他选项(如selectinload())的使用。

需要注意的是,封闭Select语句的 WHERE 和 ORDER BY 条件不针对 joinedload()渲染的表。在上面的 SQL 中可以看到,user_account表被应用了匿名别名,因此在查询中无法直接访问。这个概念在连接急切加载的禅意部分中有更详细的讨论。

提示

需要注意的是,很多对一的急切加载通常是不必要的,因为“N 加一”问题在常见情况下不太普遍。当许多对象都引用同一个相关对象时,比如许多Address对象都引用同一个User时,SQL 只会针对该User对象正常使用延迟加载而发出一次。延迟加载程序将在当前Session中通过主键查找相关对象,尽可能不发出任何 SQL。

另请参阅

连接急切加载 - 在关系加载技术中

显式连接 + 急切加载

如果我们在连接到user_account表时加载Address行,使用诸如Select.join()之类的方法来渲染 JOIN,我们还可以利用该 JOIN 来急切加载每个返回的Address对象上的Address.user属性的内容。这本质上是我们在使用“连接急切加载”,但自己渲染 JOIN。通过使用contains_eager()选项来实现这种常见用例。该选项与joinedload()非常相似,只是它假设我们自己设置了 JOIN,并且它只表示应该将 COLUMNS 子句中的附加列加载到每个返回对象的相关属性中,例如:

>>> from sqlalchemy.orm import contains_eager
>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "pkrabs")
...     .options(contains_eager(Address.user))
...     .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
...     print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT  user_account.id,  user_account.name,  user_account.fullname,
address.id  AS  id_1,  address.email_address,  address.user_id
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
WHERE  user_account.name  =  ?  ORDER  BY  address.id
[...]  ('pkrabs',)
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs

在上面的例子中,我们既过滤了user_account.name的行,也将user_account的行加载到返回行的Address.user属性中。如果我们单独应用了joinedload(),我们将得到一个不必要两次连接的 SQL 查询:

>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "pkrabs")
...     .options(joinedload(Address.user))
...     .order_by(Address.id)
... )
>>> print(stmt)  # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
SELECT  address.id,  address.email_address,  address.user_id,
user_account_1.id  AS  id_1,  user_account_1.name,  user_account_1.fullname
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
LEFT  OUTER  JOIN  user_account  AS  user_account_1  ON  user_account_1.id  =  address.user_id
WHERE  user_account.name  =  :name_1  ORDER  BY  address.id 

请参阅

关系加载技术中的两个部分:

  • 连接式预加载的禅意 - 详细描述了上述问题

  • 将显式连接/语句路由到已预加载的集合 - 使用contains_eager()

Raiseload

值得一提的一个额外的加载器策略是raiseload()。此选项用于通过导致通常是惰性加载的操作引发错误,从而完全阻止应用程序遇到 N 加 1 问题。它有两个变体,通过raiseload.sql_only选项进行控制,以阻止仅需要 SQL 的惰性加载,以及所有“加载”操作,包括仅需要查询当前Session的操作。

使用raiseload()的一种方法是在relationship()本身上进行配置,通过将relationship.lazy设置为值"raise_on_sql",以便对于特定映射,某个关系永远不会尝试发出 SQL:

>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import relationship

>>> class User(Base):
...     __tablename__ = "user_account"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     addresses: Mapped[List["Address"]] = relationship(
...         back_populates="user", lazy="raise_on_sql"
...     )

>>> class Address(Base):
...     __tablename__ = "address"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...     user: Mapped["User"] = relationship(back_populates="addresses", lazy="raise_on_sql")

使用这样的映射,应用程序被阻止了惰性加载,表示特定查询需要指定加载器策略:

>>> u1 = session.execute(select(User)).scalars().first()
SELECT  user_account.id  FROM  user_account
[...]  ()
>>> u1.addresses
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'

异常会指示应该预先加载此集合:

>>> u1 = (
...     session.execute(select(User).options(selectinload(User.addresses)))
...     .scalars()
...     .first()
... )
SELECT  user_account.id
FROM  user_account
[...]  ()
SELECT  address.user_id  AS  address_user_id,  address.id  AS  address_id
FROM  address
WHERE  address.user_id  IN  (?,  ?,  ?,  ?,  ?,  ?)
[...]  (1,  2,  3,  4,  5,  6) 

lazy="raise_on_sql"选项也试图对多对一关系变得更加智能;在上面的例子中,如果Address对象的Address.user属性未加载,但是User对象在同一个Session中是本地存在的,那么“raiseload”策略就不会引发错误。

请参阅

使用 raiseload 防止不必要的惰性加载 - 在关系加载技术中

Selectin Load

在现代 SQLAlchemy 中最有用的加载器是 selectinload() 加载器选项。该选项解决了“N 加一”问题的最常见形式,即一组对象引用相关集合的问题。selectinload() 将确保一系列对象的特定集合通过单个查询提前加载。它使用一个 SELECT 形式,在大多数情况下可以针对相关表单独发出,而无需引入 JOIN 或子查询,并且仅查询那些集合尚未加载的父对象。下面我们通过加载所有的 User 对象及其所有相关的 Address 对象来说明 selectinload();虽然我们只调用一次 Session.execute(),给定一个 select() 构造,在访问数据库时,实际上会发出两个 SELECT 语句,第二个用于获取相关的 Address 对象:

>>> from sqlalchemy.orm import selectinload
>>> stmt = select(User).options(selectinload(User.addresses)).order_by(User.id)
>>> for row in session.execute(stmt):
...     print(
...         f"{row.User.name}  ({', '.join(a.email_address for a in row.User.addresses)})"
...     )
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account  ORDER  BY  user_account.id
[...]  ()
SELECT  address.user_id  AS  address_user_id,  address.id  AS  address_id,
address.email_address  AS  address_email_address
FROM  address
WHERE  address.user_id  IN  (?,  ?,  ?,  ?,  ?,  ?)
[...]  (1,  2,  3,  4,  5,  6)
spongebob  (spongebob@sqlalchemy.org)
sandy  (sandy@sqlalchemy.org, sandy@squirrelpower.org)
patrick  ()
squidward  ()
ehkrabs  ()
pkrabs  (pearl.krabs@gmail.com, pearl@aol.com)

另见

选择 IN 加载 - 在关系加载技术中

加载连接

joinedload() 预加载策略是 SQLAlchemy 中最古老的预加载器,它通过在传递给数据库的 SELECT 语句中添加 JOIN(根据选项可能是外连接或内连接)来增强查询,然后可以加载相关联的对象。

joinedload() 策略最适合加载相关的一对多对象,因为这只需要向主实体行添加额外的列,这些列无论如何都会被检索。为了提高效率,它还接受一个选项 joinedload.innerjoin,以便在下面这种情况下使用内连接而不是外连接,我们知道所有 Address 对象都有一个关联的 User

>>> from sqlalchemy.orm import joinedload
>>> stmt = (
...     select(Address)
...     .options(joinedload(Address.user, innerjoin=True))
...     .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
...     print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT  address.id,  address.email_address,  address.user_id,  user_account_1.id  AS  id_1,
user_account_1.name,  user_account_1.fullname
FROM  address
JOIN  user_account  AS  user_account_1  ON  user_account_1.id  =  address.user_id
ORDER  BY  address.id
[...]  ()
spongebob@sqlalchemy.org spongebob
sandy@sqlalchemy.org sandy
sandy@squirrelpower.org sandy
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs

joinedload()也适用于集合,意味着一对多关系,但是它会以递归方式将主要行乘以相关项目,这样会使结果集发送的数据量呈数量级增长,用于嵌套集合和/或较大集合的情况下,应该根据情况评估其与其他选项(例如selectinload())的使用情况。

重要的是要注意,封闭Select语句的 WHERE 和 ORDER BY 条件不会针对 joinedload()渲染的表。如上所述,在 SQL 中可以看到对user_account表应用了匿名别名,因此无法直接在查询中进行地址定位。这个概念在 联接式预加载之禅 部分中有更详细的讨论。

小贴士

重要的是要注意,往往不必要进行多对一的急切加载,因为在常见情况下,“N 加一”问题不太普遍。当许多对象都引用同一个相关对象时,例如每个引用同一个User的许多Address对象时,SQL 将仅一次对该User对象使用正常的延迟加载。延迟加载程序将尽可能地在当前Session中通过主键查找相关对象,而不会在可能时发出任何 SQL。

请参见

联接式预加载 - 在 关系加载技术 中

显式连接 + 急切加载

如果我们在连接到user_account表时加载Address行,使用诸如Select.join()之类的方法来渲染连接,我们还可以利用该连接以便在每个返回的Address对象上急切加载Address.user属性的内容。这本质上是我们正在使用“联接式预加载”,但是自己渲染连接。通过使用contains_eager()选项实现了这种常见用例。该选项与joinedload()非常相似,只是它假设我们已经自己设置了连接,并且它仅指示应该将 COLUMNS 子句中的其他列加载到每个返回对象的相关属性中,例如:

>>> from sqlalchemy.orm import contains_eager
>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "pkrabs")
...     .options(contains_eager(Address.user))
...     .order_by(Address.id)
... )
>>> for row in session.execute(stmt):
...     print(f"{row.Address.email_address} {row.Address.user.name}")
SELECT  user_account.id,  user_account.name,  user_account.fullname,
address.id  AS  id_1,  address.email_address,  address.user_id
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
WHERE  user_account.name  =  ?  ORDER  BY  address.id
[...]  ('pkrabs',)
pearl.krabs@gmail.com pkrabs
pearl@aol.com pkrabs

在上述示例中,我们同时对 user_account.name 进行了行过滤,并将 user_account 的行加载到返回行的 Address.user 属性中。如果我们分别应用了 joinedload(),我们会得到一个不必要地两次连接的 SQL 查询:

>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "pkrabs")
...     .options(joinedload(Address.user))
...     .order_by(Address.id)
... )
>>> print(stmt)  # SELECT has a JOIN and LEFT OUTER JOIN unnecessarily
SELECT  address.id,  address.email_address,  address.user_id,
user_account_1.id  AS  id_1,  user_account_1.name,  user_account_1.fullname
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
LEFT  OUTER  JOIN  user_account  AS  user_account_1  ON  user_account_1.id  =  address.user_id
WHERE  user_account.name  =  :name_1  ORDER  BY  address.id 

另请参阅

关系加载技术 中的两个部分:

  • 急切加载的禅意 - 详细描述了上述问题

  • 将显式连接/语句路由到急切加载的集合中 - 使用 contains_eager()

Raiseload

还值得一提的一种额外的加载策略是 raiseload()。该选项用于通过使通常会产生惰性加载的操作引发错误来完全阻止应用程序出现 N 加一 问题。它有两种变体,通过 raiseload.sql_only 选项进行控制,以阻止需要 SQL 的惰性加载,或者包括那些只需查询当前 Session 的“加载”操作。

使用 raiseload() 的一种方法是在 relationship() 上直接配置它,通过将 relationship.lazy 设置为值 "raise_on_sql",这样对于特定映射,某个关系将永远不会尝试发出 SQL:

>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import relationship

>>> class User(Base):
...     __tablename__ = "user_account"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     addresses: Mapped[List["Address"]] = relationship(
...         back_populates="user", lazy="raise_on_sql"
...     )

>>> class Address(Base):
...     __tablename__ = "address"
...     id: Mapped[int] = mapped_column(primary_key=True)
...     user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...     user: Mapped["User"] = relationship(back_populates="addresses", lazy="raise_on_sql")

使用这样的映射,应用程序被阻止惰性加载,指示特定查询需要指定加载策略:

>>> u1 = session.execute(select(User)).scalars().first()
SELECT  user_account.id  FROM  user_account
[...]  ()
>>> u1.addresses
Traceback (most recent call last):
...
sqlalchemy.exc.InvalidRequestError: 'User.addresses' is not available due to lazy='raise_on_sql'

异常会指示应该立即加载此集合:

>>> u1 = (
...     session.execute(select(User).options(selectinload(User.addresses)))
...     .scalars()
...     .first()
... )
SELECT  user_account.id
FROM  user_account
[...]  ()
SELECT  address.user_id  AS  address_user_id,  address.id  AS  address_id
FROM  address
WHERE  address.user_id  IN  (?,  ?,  ?,  ?,  ?,  ?)
[...]  (1,  2,  3,  4,  5,  6) 

lazy="raise_on_sql" 选项还尝试智能处理多对一关系;在上述示例中,如果 Address 对象的 Address.user 属性没有加载,但是该 User 对象在同一个 Session 中本地存在,则“raiseload”策略不会引发错误。

另请参阅

使用 raiseload 防止不必要的惰性加载 - 在 关系加载技术 中

进一步阅读

原文:docs.sqlalchemy.org/en/20/tutorial/further_reading.html

下面的章节是讨论本教程中概念的主要顶级章节,更详细地描述了每个子系统的许多其他特性。

核心基础参考

  • 与引擎和连接工作

  • 模式定义语言

  • SQL 语句和表达式 API

  • SQL 数据类型对象

ORM 基础参考

  • ORM 映射类配置

  • 关系配置

  • 使用会话

  • ORM 查询指南

SQLAlchemy ORM

原文:docs.sqlalchemy.org/en/20/orm/index.html

在这里,介绍并完整描述了对象关系映射器。如果你想要使用为你自动构建的更高级别的 SQL,并且自动持久化 Python 对象,请首先转到教程。

  • ORM 快速入门

    • 声明模型

    • 创建一个引擎

    • 发出 CREATE TABLE DDL

    • 创建对象并持久化

    • 简单 SELECT

    • 带 JOIN 的 SELECT

    • 进行更改

    • 一些删除操作

    • 深入学习上述概念

  • ORM 映射类配置

    • ORM 映射类概述

    • 使用声明式映射类

    • 与 dataclasses 和 attrs 的集成

    • 将 SQL 表达式作为映射属性

    • 更改属性行为

    • 复合列类型

    • 映射类继承层次结构

    • 非传统映射

    • 配置版本计数器

    • 类映射 API

  • 关系配置

    • 基本关系模式

    • 邻接列表关系

    • 配置关系连接的方式

    • 处理大型集合

    • 集合定制和 API 详细信息

    • 特殊关系持久化模式

    • 使用传统的 'backref' 关系参数

    • 关系 API

  • ORM 查询指南

    • 为 ORM 映射类编写 SELECT 语句

    • 编写继承映射的 SELECT 语句

    • 启用 ORM 的 INSERT、UPDATE 和 DELETE 语句

    • 列加载选项

    • 关系加载技术

    • 用于查询的 ORM API 功能

    • 遗留查询 API

  • 使用会话

    • 会话基础知识

    • 状态管理

    • 级联操作

    • 事务和连接管理

    • 附加持久化技术

    • 上下文/线程本地会话

    • 使用事件跟踪查询、对象和会话的更改

    • 会话 API

  • 事件和内部原理

    • ORM 事件

    • ORM 内部

    • ORM 异常

  • ORM 扩展

    • 异步 I/O(asyncio)

    • 关联代理

    • 自动映射

    • 烘焙查询

    • 声明式扩展

    • Mypy / Pep-484 支持 ORM 映射

    • 变异跟踪

    • 排序列表

    • 水平分片

    • 混合属性

    • 可索引

    • 替代类仪器

  • ORM 示例

    • 映射示例

    • 继承映射示例

    • 特殊 API

    • 扩展 ORM

ORM 快速入门

原文:docs.sqlalchemy.org/en/20/orm/quickstart.html

对于想要快速了解基本 ORM 使用情况的新用户,这里提供了 SQLAlchemy 统一教程中使用的映射和示例的缩写形式。这里的代码可以从干净的命令行完全运行。

由于本节中的描述故意非常简短,请继续阅读完整的 SQLAlchemy 统一教程以获得对这里所说明的每个概念更深入的描述。

从 2.0 版本开始更改:ORM 快速入门已更新为最新的PEP 484兼容功能,使用包括mapped_column()在内的新构造。有关迁移信息,请参见 ORM 声明模型部分。

声明模型

在这里,我们定义模块级别的构造,这些构造将形成我们将从数据库查询的结构。这个结构被称为声明式映射,它一次定义了 Python 对象模型,以及描述存在或将存在于特定数据库中的真实 SQL 表的数据库元数据:

>>> from typing import List
>>> from typing import Optional
>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import String
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship

>>> class Base(DeclarativeBase):
...     pass

>>> class User(Base):
...     __tablename__ = "user_account"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     name: Mapped[str] = mapped_column(String(30))
...     fullname: Mapped[Optional[str]]
...
...     addresses: Mapped[List["Address"]] = relationship(
...         back_populates="user", cascade="all, delete-orphan"
...     )
...
...     def __repr__(self) -> str:
...         return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

>>> class Address(Base):
...     __tablename__ = "address"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     email_address: Mapped[str]
...     user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...
...     user: Mapped["User"] = relationship(back_populates="addresses")
...
...     def __repr__(self) -> str:
...         return f"Address(id={self.id!r}, email_address={self.email_address!r})"

映射始于一个基类,这个基类上面称为Base,并且是通过对DeclarativeBase类进行简单子类化来创建的。

然后通过对Base进行子类化来创建单独的映射类。一个映射类通常指的是单个特定的数据库表,其名称通过使用__tablename__类级别属性指示。

接下来,通过添加包含称为Mapped的特殊类型注释的属性来声明表的一部分列。每个属性的名称对应于要成为数据库表的一部分的列。每个列的数据类型首先从与每个Mapped注释相关联的 Python 数据类型中获取;int用于INTEGERstr用于VARCHAR,等等。空值性取决于是否使用了Optional[]类型修饰符。可以使用右侧mapped_column()指令中的 SQLAlchemy 类型对象指示更具体的类型信息,例如上面在User.name列中使用的String数据类型。可以使用类型注释映射来自定义 Python 类型和 SQL 类型之间的关联。

mapped_column()指令用于所有需要更具体定制的基于列的属性。除了类型信息外,此指令还接受各种参数,指示有关数据库列的特定详细信息,包括服务器默认值和约束信息,例如在主键和外键中的成员资格。mapped_column()指令接受的参数是 SQLAlchemy Column类所接受的参数的一个超集,该类由 SQLAlchemy 核心用于表示数据库列。

所有 ORM 映射类都要求至少声明一个列作为主键的一部分,通常是通过在那些应该成为主键的mapped_column()对象上使用Column.primary_key参数来实现的。在上面的示例中,User.idAddress.id列被标记为主键。

综合考虑,字符串表名称以及列声明列表的组合在 SQLAlchemy 中被称为 table metadata。在 SQLAlchemy 统一教程的处理数据库元数据中介绍了如何使用核心和 ORM 方法设置表元数据。上述映射是所谓的注释声明表配置的示例。

Mapped 的其他变体可用,最常见的是上面指示的 relationship() 构造。与基于列的属性相比,relationship() 表示两个 ORM 类之间的关联。在上面的示例中,User.addressesUserAddress 连接起来,Address.userAddressUser 连接起来。relationship() 构造介绍于 SQLAlchemy 统一教程 的 处理 ORM 相关对象 部分。

最后,上面的示例类包括一个 __repr__() 方法,这并非必需,但对调试很有用。映射类可以使用诸如 __repr__() 之类的方法自动生成,使用数据类。有关数据类映射的更多信息,请参阅 声明式数据类映射。

创建一个引擎

Engine 是一个工厂,可以为我们创建新的数据库连接,还在 连接池 中保存连接以便快速重用。出于学习目的,我们通常使用一个 SQLite 内存数据库方便起见:

>>> from sqlalchemy import create_engine
>>> engine = create_engine("sqlite://", echo=True)

小贴士

echo=True 参数表示连接发出的 SQL 将被记录到标准输出。

Engine 的完整介绍从 建立连接 - 引擎 开始。

发出 CREATE TABLE DDL

利用我们的表格元数据和引擎,我们可以一次性在目标 SQLite 数据库中生成我们的模式,使用的方法是 MetaData.create_all()

>>> Base.metadata.create_all(engine)
BEGIN  (implicit)
PRAGMA  main.table_...info("user_account")
...
PRAGMA  main.table_...info("address")
...
CREATE  TABLE  user_account  (
  id  INTEGER  NOT  NULL,
  name  VARCHAR(30)  NOT  NULL,
  fullname  VARCHAR,
  PRIMARY  KEY  (id)
)
...
CREATE  TABLE  address  (
  id  INTEGER  NOT  NULL,
  email_address  VARCHAR  NOT  NULL,
  user_id  INTEGER  NOT  NULL,
  PRIMARY  KEY  (id),
  FOREIGN  KEY(user_id)  REFERENCES  user_account  (id)
)
...
COMMIT 

我们刚刚写的那小段 Python 代码发生了很多事情。要完整了解表格元数据的情况,请在教程中继续阅读 处理数据库元数据 部分。

创建对象并持久化

现在我们已经准备好向数据库插入数据了。我们通过创建UserAddress类的实例来实现这一目标,这些类已经通过声明性映射过程自动建立了__init__()方法。然后,我们使用一个名为 Session 的对象将它们传递给数据库,该对象利用Engine与数据库进行交互。这里使用了Session.add_all()方法一次添加多个对象,并且Session.commit()方法将被用来提交数据库中的任何挂起更改,然后提交当前的数据库事务,无论何时使用Session时,该事务始终处于进行中:

>>> from sqlalchemy.orm import Session

>>> with Session(engine) as session:
...     spongebob = User(
...         name="spongebob",
...         fullname="Spongebob Squarepants",
...         addresses=[Address(email_address="spongebob@sqlalchemy.org")],
...     )
...     sandy = User(
...         name="sandy",
...         fullname="Sandy Cheeks",
...         addresses=[
...             Address(email_address="sandy@sqlalchemy.org"),
...             Address(email_address="sandy@squirrelpower.org"),
...         ],
...     )
...     patrick = User(name="patrick", fullname="Patrick Star")
...
...     session.add_all([spongebob, sandy, patrick])
...
...     session.commit()
BEGIN  (implicit)
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...]  ('spongebob',  'Spongebob Squarepants')
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...]  ('sandy',  'Sandy Cheeks')
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...]  ('patrick',  'Patrick Star')
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...]  ('spongebob@sqlalchemy.org',  1)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...]  ('sandy@sqlalchemy.org',  2)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...]  ('sandy@squirrelpower.org',  2)
COMMIT 

提示

建议以上述上下文管理器风格使用Session,即使用 Python 的 with: 语句。Session 对象代表了活动的数据库资源,因此确保在完成一系列操作时将其关闭是很好的。在下一节中,我们将保持一个Session仅用于说明目的。

关于创建Session的基础知识请参见使用 ORM Session 执行,更多内容请查看使用 Session 的基础知识。

然后,在使用 ORM 工作单元模式插入行中介绍了一些基本持久性操作的变体。

简单的 SELECT

在数据库中有一些行之后,这是发出 SELECT 语句以加载一些对象的最简单形式。要创建 SELECT 语句,我们使用 select() 函数创建一个新的 Select 对象,然后使用一个 Session 调用它。在查询 ORM 对象时经常有用的方法是 Session.scalars() 方法,它将返回一个 ScalarResult 对象,该对象将遍历我们已选择的 ORM 对象:

>>> from sqlalchemy import select

>>> session = Session(engine)

>>> stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))

>>> for user in session.scalars(stmt):
...     print(user)
BEGIN  (implicit)
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  IN  (?,  ?)
[...]  ('spongebob',  'sandy')
User(id=1, name='spongebob', fullname='Spongebob Squarepants')
User(id=2, name='sandy', fullname='Sandy Cheeks')

上述查询还使用了 Select.where() 方法添加 WHERE 条件,并且还使用了 SQLAlchemy 类似列的构造中的 ColumnOperators.in_() 方法来使用 SQL IN 操作符。

有关如何选择对象和单独列的更多细节请参见选择 ORM 实体和列。

使用 JOIN 进行 SELECT

在一次性查询多个表格是非常常见的,在 SQL 中,JOIN 关键字是这种情况的主要方式。Select 构造使用 Select.join() 方法创建连接:

>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "sandy")
...     .where(Address.email_address == "sandy@sqlalchemy.org")
... )
>>> sandy_address = session.scalars(stmt).one()
SELECT  address.id,  address.email_address,  address.user_id
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
WHERE  user_account.name  =  ?  AND  address.email_address  =  ?
[...]  ('sandy',  'sandy@sqlalchemy.org')
>>> sandy_address
Address(id=2, email_address='sandy@sqlalchemy.org')

上述查询演示了多个 WHERE 条件的使用,这些条件会自动使用 AND 进行链接,以及如何使用 SQLAlchemy 类似列对象创建“相等性”比较,这使用了重写的 Python 方法 ColumnOperators.__eq__() 来生成 SQL 条件对象。

有关上述概念的更多背景信息在 WHERE 子句和明确的 FROM 子句和 JOIN 处。

进行更改

Session对象与我们的 ORM 映射类UserAddress结合使用,自动跟踪对对象的更改,这些更改将在下次Session flush 时生成 SQL 语句。 在下面,我们更改了与“sandy”关联的一个电子邮件地址,并在发出 SELECT 以检索“patrick”的行后向“patrick”添加了一个新的电子邮件地址:

>>> stmt = select(User).where(User.name == "patrick")
>>> patrick = session.scalars(stmt).one()
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('patrick',)
>>> patrick.addresses.append(Address(email_address="patrickstar@sqlalchemy.org"))
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,  address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (3,)
>>> sandy_address.email_address = "sandy_cheeks@sqlalchemy.org"

>>> session.commit()
UPDATE  address  SET  email_address=?  WHERE  address.id  =  ?
[...]  ('sandy_cheeks@sqlalchemy.org',  2)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)
[...]  ('patrickstar@sqlalchemy.org',  3)
COMMIT 

注意当我们访问patrick.addresses时,会发出一个 SELECT。 这称为延迟加载。 关于使用更多或更少 SQL 访问相关项目的不同方式的背景介绍在加载策略中引入。

有关 ORM 数据操作的详细说明始于使用 ORM 进行数据操作。

一些删除

一切都必须有个了结,就像我们的一些数据库行一样 - 这里是两种不同形式的删除的快速演示,这两种删除根据特定用例的不同而重要。

首先,我们将从sandy用户中删除一个Address对象。 当Session下次 flush 时,这将导致该行被删除。 此行为是我们在映射中配置的称为删除级联的东西。 我们可以使用Session.get()按主键获取sandy对象的句柄,然后使用该对象:

>>> sandy = session.get(User, 2)
BEGIN  (implicit)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,  user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (2,)
>>> sandy.addresses.remove(sandy_address)
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,  address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (2,) 

上面的最后一个 SELECT 是延迟加载操作进行,以便加载sandy.addresses集合,以便我们可以删除sandy_address成员。有其他方法可以完成这一系列操作,这些方法不会生成太多的 SQL。

我们可以选择发出 DELETE SQL,以删除到目前为止已更改的内容,而不提交事务,使用Session.flush()方法:

>>> session.flush()
DELETE  FROM  address  WHERE  address.id  =  ?
[...]  (2,) 

接下来,我们将完全删除“patrick”用户。 对于对象本身的顶级删除,我们使用Session.delete()方法; 此方法实际上不执行删除,而是设置对象将在下次 flush 时被删除。 该操作还将根据我们配置的级联选项级联到相关对象,本例中为相关的Address对象:

>>> session.delete(patrick)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,  user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (3,)
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,  address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (3,) 

在这种特殊情况下,Session.delete()方法发出了两个 SELECT 语句,即使它没有发出 DELETE,这可能看起来令人惊讶。这是因为当该方法去检查对象时,发现patrick对象已经过期,这是在我们上次调用Session.commit()时发生的,发出的 SQL 是为了重新从新事务加载行。这种过期是可选的,并且在正常使用中,我们经常会在不适用的情况下关闭它。

为了说明被删除的行,这里是提交:

>>> session.commit()
DELETE  FROM  address  WHERE  address.id  =  ?
[...]  (4,)
DELETE  FROM  user_account  WHERE  user_account.id  =  ?
[...]  (3,)
COMMIT 

教程讨论了 ORM 删除,详见使用工作单元模式删除 ORM 对象。对象过期的背景信息在过期/刷新;级联在级联中进行了深入讨论。

深入学习上述概念

对于新用户来说,上面的部分可能是一个快速浏览。上面的每一步中都有许多重要的概念没有涵盖到。通过快速了解事物的外观,建议通过 SQLAlchemy 统一教程逐步学习,以获得对上面所发生的事物的坚实的工作知识。祝你好运!

声明模型

在这里,我们定义了将构成我们从数据库查询的模块级构造。这个结构被称为声明性映射,它一次定义了 Python 对象模型以及描述真实 SQL 表的数据库元数据,这些表存在或将存在于特定数据库中:

>>> from typing import List
>>> from typing import Optional
>>> from sqlalchemy import ForeignKey
>>> from sqlalchemy import String
>>> from sqlalchemy.orm import DeclarativeBase
>>> from sqlalchemy.orm import Mapped
>>> from sqlalchemy.orm import mapped_column
>>> from sqlalchemy.orm import relationship

>>> class Base(DeclarativeBase):
...     pass

>>> class User(Base):
...     __tablename__ = "user_account"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     name: Mapped[str] = mapped_column(String(30))
...     fullname: Mapped[Optional[str]]
...
...     addresses: Mapped[List["Address"]] = relationship(
...         back_populates="user", cascade="all, delete-orphan"
...     )
...
...     def __repr__(self) -> str:
...         return f"User(id={self.id!r}, name={self.name!r}, fullname={self.fullname!r})"

>>> class Address(Base):
...     __tablename__ = "address"
...
...     id: Mapped[int] = mapped_column(primary_key=True)
...     email_address: Mapped[str]
...     user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id"))
...
...     user: Mapped["User"] = relationship(back_populates="addresses")
...
...     def __repr__(self) -> str:
...         return f"Address(id={self.id!r}, email_address={self.email_address!r})"

映射始于一个基类,上面称为Base,通过对DeclarativeBase类进行简单的子类化来创建。

通过对Base进行子类化,然后创建个体映射类。一个映射类通常指的是一个特定的数据库表,其名称是通过使用__tablename__类级属性指示的。

接下来,声明表中的列,通过添加包含一个特殊的类型注释称为Mapped的属性来实现。每个属性的名称对应于要成为数据库表的列。每个列的数据类型首先取自与每个Mapped注释相关联的 Python 数据类型;对于 INTEGER 使用 int,对于 VARCHAR 使用 str 等。可选性取决于是否使用了 Optional[] 类型修饰符。可以使用右侧的 SQLAlchemy 类型对象指示更具体的类型信息,例如上面在 User.name 列中使用的 String 数据类型。Python 类型和 SQL 类型之间的关联可以使用 type annotation map 进行定制。

mapped_column() 指令用于所有需要更具体定制的基于列的属性。除了类型信息外,该指令还接受各种参数,指示有关数据库列的特定细节,包括服务器默认值和约束信息,例如主键和外键的成员资格。mapped_column() 指令接受了 SQLAlchemy Column 类接受的参数的超集,该类由 SQLAlchemy Core 用于表示数据库列。

所有的 ORM 映射类都需要至少声明一个列作为主键的一部分,通常是通过在应该成为键的那些mapped_column()对象上使用Column.primary_key参数来实现的。在上面的示例中,User.idAddress.id 列被标记为主键。

综合起来,SQLAlchemy 中一个字符串表名和列声明列表的组合被称为 table metadata。在 SQLAlchemy 统一教程中介绍了使用 Core 和 ORM 方法设置表元数据的方法,在 Working with Database Metadata 章节中。上述映射是 Annotated Declarative Table 配置的示例。

还有其他Mapped的变体可用,最常见的是上面指示的relationship()构造。与基于列的属性相反,relationship()表示两个 ORM 类之间的链接。在上面的示例中,User.addressesUser链接到AddressAddress.userAddress链接到Userrelationship()构造在 SQLAlchemy 统一教程中的使用 ORM 相关对象中进行介绍。

最后,上面的示例类包括一个 __repr__() 方法,虽然不是必需的,但对于调试很有用。映射类可以使用诸如 __repr__() 这样的方法自动生成,使用数据类。有关数据类映射的更多信息,请参阅声明性数据类映射。

创建引擎

Engine是一个能够为我们创建新数据库连接的工厂,它还将连接保留在连接池中以供快速重用。出于学习目的,我们通常使用 SQLite 内存数据库以方便起见:

>>> from sqlalchemy import create_engine
>>> engine = create_engine("sqlite://", echo=True)

提示

echo=True 参数表示连接发出的 SQL 将被记录到标准输出。

Engine的全面介绍始于建立连接 - 引擎。

发出 CREATE TABLE DDL

使用我们的表元数据和引擎,我们可以一次性在目标 SQLite 数据库中生成我们的模式,使用一种叫做MetaData.create_all()的方法:

>>> Base.metadata.create_all(engine)
BEGIN  (implicit)
PRAGMA  main.table_...info("user_account")
...
PRAGMA  main.table_...info("address")
...
CREATE  TABLE  user_account  (
  id  INTEGER  NOT  NULL,
  name  VARCHAR(30)  NOT  NULL,
  fullname  VARCHAR,
  PRIMARY  KEY  (id)
)
...
CREATE  TABLE  address  (
  id  INTEGER  NOT  NULL,
  email_address  VARCHAR  NOT  NULL,
  user_id  INTEGER  NOT  NULL,
  PRIMARY  KEY  (id),
  FOREIGN  KEY(user_id)  REFERENCES  user_account  (id)
)
...
COMMIT 

刚才我们编写的那段 Python 代码发生了很多事情。要完整了解表元数据的情况,请参阅使用数据库元数据中的教程。

创建对象并持久化

我们现在可以将数据插入到数据库中了。我们通过创建UserAddress类的实例来实现这一点,这些类已经通过声明映射过程自动创建了__init__()方法。然后,我们使用一个称为 Session 的对象将它们传递给数据库,该对象使用Engine与数据库进行交互。这里使用Session.add_all()方法一次添加多个对象,并且将使用Session.commit()方法刷新数据库中的任何待处理更改,然后提交当前的数据库事务,该事务始终在使用Session时处于进行中:

>>> from sqlalchemy.orm import Session

>>> with Session(engine) as session:
...     spongebob = User(
...         name="spongebob",
...         fullname="Spongebob Squarepants",
...         addresses=[Address(email_address="spongebob@sqlalchemy.org")],
...     )
...     sandy = User(
...         name="sandy",
...         fullname="Sandy Cheeks",
...         addresses=[
...             Address(email_address="sandy@sqlalchemy.org"),
...             Address(email_address="sandy@squirrelpower.org"),
...         ],
...     )
...     patrick = User(name="patrick", fullname="Patrick Star")
...
...     session.add_all([spongebob, sandy, patrick])
...
...     session.commit()
BEGIN  (implicit)
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...]  ('spongebob',  'Spongebob Squarepants')
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...]  ('sandy',  'Sandy Cheeks')
INSERT  INTO  user_account  (name,  fullname)  VALUES  (?,  ?)  RETURNING  id
[...]  ('patrick',  'Patrick Star')
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...]  ('spongebob@sqlalchemy.org',  1)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...]  ('sandy@sqlalchemy.org',  2)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)  RETURNING  id
[...]  ('sandy@squirrelpower.org',  2)
COMMIT 

提示

建议像上面那样使用 Python 的 with: 语句,即使用上下文管理器样式使用SessionSession 对象代表着活跃的数据库资源,所以当一系列操作完成时,确保关闭它是很好的。在下一节中,我们将保持Session处于打开状态,仅用于说明目的。

创建Session的基础知识请参考使用 ORM Session 执行,更多内容请参考使用 Session 的基础知识。

接下来,介绍了一些基本持久化操作的变体,请参阅使用 ORM 工作单元模式插入行。

简单的 SELECT

在数据库中有一些行时,这是发出 SELECT 语句以加载一些对象的最简单形式。要创建 SELECT 语句,我们使用select() 函数创建一个新的Select 对象,然后使用Session 调用它。查询 ORM 对象时经常有用的方法是Session.scalars() 方法,它将返回一个ScalarResult 对象,该对象将迭代我们选择的 ORM 对象:

>>> from sqlalchemy import select

>>> session = Session(engine)

>>> stmt = select(User).where(User.name.in_(["spongebob", "sandy"]))

>>> for user in session.scalars(stmt):
...     print(user)
BEGIN  (implicit)
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  IN  (?,  ?)
[...]  ('spongebob',  'sandy')
User(id=1, name='spongebob', fullname='Spongebob Squarepants')
User(id=2, name='sandy', fullname='Sandy Cheeks')

上述查询还使用了Select.where() 方法添加 WHERE 条件,并且还使用了所有 SQLAlchemy 列对象的一部分的ColumnOperators.in_() 方法来使用 SQL IN 操作符。

如何选择对象和单独列的更多详细信息请参阅选择 ORM 实体和列。

使用 JOIN 的 SELECT

在 SQL 中,一次查询多个表是非常常见的,而 JOIN 关键字是实现这一目的的主要方法。Select 构造函数使用Select.join() 方法创建连接:

>>> stmt = (
...     select(Address)
...     .join(Address.user)
...     .where(User.name == "sandy")
...     .where(Address.email_address == "sandy@sqlalchemy.org")
... )
>>> sandy_address = session.scalars(stmt).one()
SELECT  address.id,  address.email_address,  address.user_id
FROM  address  JOIN  user_account  ON  user_account.id  =  address.user_id
WHERE  user_account.name  =  ?  AND  address.email_address  =  ?
[...]  ('sandy',  'sandy@sqlalchemy.org')
>>> sandy_address
Address(id=2, email_address='sandy@sqlalchemy.org')

上述查询示例说明了多个 WHERE 条件如何自动使用 AND 连接,并且展示了如何使用 SQLAlchemy 列对象创建“相等性”比较,该比较使用了重载的 Python 方法ColumnOperators.__eq__()来生成 SQL 条件对象。

以上概念的更多背景可在 WHERE 子句和显式 FROM 子句和 JOIN 处找到。

进行更改

Session 对象与我们的 ORM 映射类 UserAddress 一起,会自动跟踪对象的更改,这些更改会导致 SQL 语句在下次 Session 刷新时被发出。下面,我们更改了与“sandy”关联的一个电子邮件地址,并在发出 SELECT 以检索“patrick”的行之后,向“patrick”添加了一个新的电子邮件地址:

>>> stmt = select(User).where(User.name == "patrick")
>>> patrick = session.scalars(stmt).one()
SELECT  user_account.id,  user_account.name,  user_account.fullname
FROM  user_account
WHERE  user_account.name  =  ?
[...]  ('patrick',)
>>> patrick.addresses.append(Address(email_address="patrickstar@sqlalchemy.org"))
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,  address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (3,)
>>> sandy_address.email_address = "sandy_cheeks@sqlalchemy.org"

>>> session.commit()
UPDATE  address  SET  email_address=?  WHERE  address.id  =  ?
[...]  ('sandy_cheeks@sqlalchemy.org',  2)
INSERT  INTO  address  (email_address,  user_id)  VALUES  (?,  ?)
[...]  ('patrickstar@sqlalchemy.org',  3)
COMMIT 

注意当我们访问patrick.addresses时,会发出一个 SELECT。这被称为延迟加载。有关使用更多或更少的 SQL 访问相关项目的不同方法的背景介绍,请参阅加载器策略。

有关使用 ORM 进行数据操作的详细说明,请参阅 ORM 数据操作。

一些删除操作

万物都有尽头,就像我们的一些数据库行一样 - 这里快速演示了两种不同形式的删除,根据特定用例的重要性而定。

首先,我们将从sandy用户中删除一个Address对象。当Session下次刷新时,这将导致该行被删除。这种行为是我们在映射中配置的,称为级联删除。我们可以使用 Session.get() 按主键获取到sandy对象,然后操作该对象:

>>> sandy = session.get(User, 2)
BEGIN  (implicit)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,  user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (2,)
>>> sandy.addresses.remove(sandy_address)
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,  address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (2,) 

上面的最后一个 SELECT 是为了进行延迟加载 操作,以便加载sandy.addresses集合,以便我们可以删除sandy_address成员。还有其他方法可以执行这一系列操作,不会发出太多的 SQL。

我们可以选择发出针对到目前为止被更改的 DELETE SQL,而不提交事务,使用 Session.flush() 方法:

>>> session.flush()
DELETE  FROM  address  WHERE  address.id  =  ?
[...]  (2,) 

接下来,我们将完全删除“patrick”用户。对于对象的顶级删除,我们使用Session.delete()方法;这个方法实际上并不执行删除操作,而是设置对象在下一次刷新时将被删除。该操作还会根据我们配置的级联选项级联到相关对象,本例中是关联的Address对象:

>>> session.delete(patrick)
SELECT  user_account.id  AS  user_account_id,  user_account.name  AS  user_account_name,  user_account.fullname  AS  user_account_fullname
FROM  user_account
WHERE  user_account.id  =  ?
[...]  (3,)
SELECT  address.id  AS  address_id,  address.email_address  AS  address_email_address,  address.user_id  AS  address_user_id
FROM  address
WHERE  ?  =  address.user_id
[...]  (3,) 

在这种特殊情况下,Session.delete()方法发出了两个 SELECT 语句,即使它没有发出 DELETE,这可能看起来令人惊讶。这是因为当方法检查对象时,发现patrick对象已经过期,这是在我们上次调用Session.commit()时发生的,发出的 SQL 是为了从新事务重新加载行。这种过期是可选的,在正常使用中,我们通常会在不适用的情况下关闭它。

要说明被删除的行,请看这个提交:

>>> session.commit()
DELETE  FROM  address  WHERE  address.id  =  ?
[...]  (4,)
DELETE  FROM  user_account  WHERE  user_account.id  =  ?
[...]  (3,)
COMMIT 

本教程讨论了 ORM 删除操作,详情请见使用工作单元模式删除 ORM 对象。关于对象过期的背景信息请参考过期/刷新;级联操作在 Cascades 中有详细讨论。

深入学习上述概念

对于新用户来说,上述部分可能是一场令人眼花缭乱的旅程。每个步骤中都有许多重要概念没有涵盖。快速了解事物的外观后,建议通过 SQLAlchemy 统一教程来深入了解上述内容。祝好运!

ORM 映射类配置

原文:docs.sqlalchemy.org/en/20/orm/mapper_config.html

ORM 配置的详细参考,不包括关系,关系详细说明在关系配置。

要快速查看典型的 ORM 配置,请从 ORM 快速入门开始。

要了解 SQLAlchemy 实现的对象关系映射概念,请先查看 SQLAlchemy 统一教程,在使用 ORM 声明形式定义表元数据中介绍。

  • ORM 映射类概述

    • ORM 映射风格

      • 声明性映射

      • 命令式映射

    • 映射类基本组件

      • 待映射的类

      • 表或其他来自子句对象

      • 属性字典

      • 其他映射器配置参数

    • 映射类行为

      • 默认构造函数

      • 跨加载保持非映射状态

      • 映射类、实例和映射器的运行时内省

        • 映射器对象的检查

        • 映射实例的检查

  • 使用声明性映射类

    • 声明性映射风格

      • 使用声明性基类

      • 使用装饰器的声明性映射(无声明性基类)

    • 使用声明性配置表

      • 带有 mapped_column() 的声明性表

        • 使用带注释的声明性表(mapped_column()的类型注释形式)

        • 访问表和元数据

        • 声明性表配置

        • 使用声明性表的显式模式名称

        • 为声明式映射的列设置加载和持久化选项

        • 显式命名声明式映射列

        • 将额外列添加到现有的声明式映射类

      • 使用命令式表进行声明式(即混合声明式)

        • 映射表列的替代属性名

        • 为命令式表列应用加载、持久化和映射选项

      • 使用反射表进行声明式映射

        • 使用延迟反射

        • 使用自动映射

        • 从反射表自动化列命名方案

        • 映射到显式主键列集合

        • 映射表列的子集

    • 声明式映射器配置

      • 使用声明式定义映射属性

      • 声明式的映射器配置选项

        • 动态构建映射器参数
      • 其他声明式映射指令

        • __declare_last__()

        • __declare_first__()

        • metadata

        • __abstract__

        • __table_cls__

    • 使用混合组合映射层次结构

      • 增强基类

      • 混合使用列

      • 混合使用关系

      • _orm.column_property() 和其他 _orm.MapperProperty 类中混合使用

      • 使用混合和基类进行映射继承模式

        • 使用 _orm.declared_attr() 与继承 TableMapper 参数

        • 使用 _orm.declared_attr() 生成表特定的继承列

      • 从多个混合类组合表/映射器参数

      • 使用命名约定在混合类上创建索引和约束

  • 与 dataclasses 和 attrs 集成

    • 声明式数据类映射

      • 类级别功能配置

      • 属性配置

        • 列默认值

        • 与 Annotated 集成

      • 使用混合类和抽象超类

      • 关系配置

      • 使用未映射的数据类字段

      • 与 Pydantic 等替代数据类提供者集成

    • 将 ORM 映射应用于现有的数据类(传统数据类使用)

      • 使用声明式与命令式表映射映射预先存在的数据类

      • 使用声明式样式字段映射预先存在的数据类

        • 使用预先存在的数据类的声明式混合类
      • 使用命令式映射映射预先存在的数据类

    • 将 ORM 映射应用于现有的 attrs 类

      • 使用声明式“命令式表”映射映射属性

      • 使用命令式映射映射属性

  • SQL 表达式作为映射属性

    • 使用混合类

    • 使用 column_property

      • 将 column_property() 添加到现有的声明式映射类

      • 在映射时从列属性组合

      • 使用 column_property() 进行列推迟

    • 使用普通描述符

    • 查询时将 SQL 表达式作为映射属性

  • 更改属性行为

    • 简单验证器

      • validates()
    • 在核心级别使用自定义数据类型

    • 使用描述符和混合物

    • 同义词

      • synonym()
    • 操作符定制

  • 复合列类型

    • 使用映射的复合列类型

    • 复合体的其他映射形式

      • 直接映射列,然后传递给复合体

      • 直接映射列,将属性名称传递给复合体

      • 命令映射和命令表

    • 使用传统非数据类

    • 跟踪复合体上的原位变化

    • 重新定义复合体的比较操作

    • 嵌套复合体

    • 复合体 API

      • composite()
  • 映射类继承层次结构

    • 联接表继承

      • 与联接继承相关的关系

      • 加载联接继承映射

    • 单表继承

      • 使用 use_existing_column 解决列冲突

      • 与单表继承相关的关系

      • 使用 polymorphic_abstract 构建更深层次的层次结构

      • 加载单表继承映射

    • 具体表继承

      • 具体多态加载配置

      • 抽象具体类

      • 经典和半经典具体多态配置

      • 具体继承关系的关系

      • 加载具体继承映射

  • 非传统映射

    • 将类映射到多个表

    • 将类映射到任意子查询

    • 一个类的多个映射器

  • 配置版本计数器

    • 简单版本计数

    • 自定义版本计数器/类型

    • 服务器端版本计数器

    • 编程或条件版本计数器

  • 类映射 API

    • registry

      • registry.__init__()

      • registry.as_declarative_base()

      • registry.configure()

      • registry.dispose()

      • registry.generate_base()

      • registry.map_declaratively()

      • registry.map_imperatively()

      • registry.mapped()

      • registry.mapped_as_dataclass()

      • registry.mappers

      • registry.update_type_annotation_map()

    • add_mapped_attribute()

    • column_property()

    • declarative_base()

    • declarative_mixin()

    • as_declarative()

    • mapped_column()

    • declared_attr

      • declared_attr.cascading

      • declared_attr.directive

    • DeclarativeBase

      • DeclarativeBase.__mapper__

      • DeclarativeBase.__mapper_args__

      • DeclarativeBase.__table__

      • DeclarativeBase.__table_args__

      • DeclarativeBase.__tablename__

      • DeclarativeBase.metadata

      • DeclarativeBase.registry

    • DeclarativeBaseNoMeta

      • DeclarativeBaseNoMeta.__mapper__

      • DeclarativeBaseNoMeta.__mapper_args__

      • DeclarativeBaseNoMeta.__table__

      • DeclarativeBaseNoMeta.__table_args__

      • DeclarativeBaseNoMeta.__tablename__

      • DeclarativeBaseNoMeta.metadata

      • DeclarativeBaseNoMeta.registry

    • has_inherited_table()

    • synonym_for()

    • object_mapper()

    • class_mapper()

    • configure_mappers()

    • clear_mappers()

    • identity_key()

    • polymorphic_union()

    • orm_insert_sentinel()

    • reconstructor()

    • Mapper

      • Mapper.__init__()

      • Mapper.add_properties()

      • Mapper.add_property()

      • Mapper.all_orm_descriptors

      • Mapper.attrs

      • Mapper.base_mapper

      • Mapper.c

      • Mapper.cascade_iterator()

      • Mapper.class_

      • Mapper.class_manager

      • Mapper.column_attrs

      • Mapper.columns

      • Mapper.common_parent()

      • Mapper.composites

      • Mapper.concrete

      • Mapper.configured

      • Mapper.entity

      • Mapper.get_property()

      • Mapper.get_property_by_column()

      • Mapper.identity_key_from_instance()

      • Mapper.identity_key_from_primary_key()

      • Mapper.identity_key_from_row()

      • Mapper.inherits

      • Mapper.is_mapper

      • Mapper.is_sibling()

      • Mapper.isa()

      • Mapper.iterate_properties

      • Mapper.local_table

      • Mapper.mapped_table

      • Mapper.mapper

      • Mapper.non_primary

      • Mapper.persist_selectable

      • Mapper.polymorphic_identity

      • Mapper.polymorphic_iterator()

      • Mapper.polymorphic_map

      • Mapper.polymorphic_on

      • Mapper.primary_key

      • Mapper.primary_key_from_instance()

      • Mapper.primary_mapper()

      • Mapper.relationships

      • Mapper.selectable

      • Mapper.self_and_descendants

      • Mapper.single

      • Mapper.synonyms

      • Mapper.tables

      • Mapper.validators

      • Mapper.with_polymorphic_mappers

    • MappedAsDataclass

    • MappedClassProtocol

ORM 映射类概述

原文:docs.sqlalchemy.org/en/20/orm/mapping_styles.html

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对象,最常见的替代对象是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")

提示

声明式数据类映射功能提供了一种通过使用 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,更类似于反序列化,比如反序列化,而不是初始构造。对象的大部分重要状态不是首次组装,而是重新从数据库行加载。

因此,为了在对象内部维护不属于存储到数据库的数据的状态,使得当对象加载和构造时都存在这些状态,有两种通用方法如下所述。

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

    对于简单的属性,这是最简单的方法,也是最不容易出错的方法。例如,如果一个对象 PointPoint.xPoint.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
    

    使用动态描述符的优点是值每次都会计算,这意味着它会根据底层属性(在本例中为 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__
    

    这个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 构造。

声明式映射

Declarative Mapping 是在现代 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 Declarative Models。

基类指的是一个维护一组相关映射类的 registry 对象,以及一个保留了一组将类映射到其中的 Table 对象的 MetaData 对象。

主要的 Declarative 映射风格在以下各节中进一步详细说明:

  • 使用声明基类 - 使用基类进行声明式映射。

  • 使用装饰器进行声明式映射(无声明基类) - 使用装饰器进行声明式映射,而不是使用基类。

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

  • mapped_column()的声明式表 - 在映射类内联声明表列,使用mapped_column()指令(或者在遗留形式中,直接使用Column对象)。mapped_column()指令也可以选择性地与类型注解结合使用,使用Mapped类可以直接提供有关映射列的一些细节。列指令与__tablename__和可选的__table_args__类级指令的组合将允许声明式映射过程构造一个要映射的Table对象。

  • 声明式与命令式表(也称为混合声明式) - 不是分别指定表名和属性,而是将明确构造的Table对象与否则以声明方式映射的类相关联。这种映射风格是“声明式”和“命令式”映射的混合体,并适用于将类映射到反射的Table对象,以及将类映射到现有 Core 构造,如连接和子查询。

声明式映射的文档继续在使用声明性映射类 ### 命令式映射

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

提示

命令式映射形式是 SQLAlchemy 在 2006 年的最初版本中少用的一种映射形式。它本质上是绕过声明式系统提供一种更“精简”的映射系统,不提供现代特性,如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的行为”时,这也包括在使用声明式系统时 - 它仍然被使用,只是在幕后。 ### 声明式映射

声明式映射是在现代 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的参数来自给定的映射形式,包括传递给registry.map_imperatively()的参数,用于命令式映射,或者使用声明式系统时,来自与被映射的表列、SQL 表达式和关系相关联的参数以及属性的参数,如 mapper_args

Mapper类寻找的四类常规配置信息如下:

待映射的类

这是我们在应用程序中构造的类。通常情况下,这个类的结构没有限制。[1]当一个 Python 类被映射时,该类只能有一个Mapper对象。[2]

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

当使用命令式映射风格时,类直接作为map_imperatively.class_参数传递。

表格或其他来源子句对象

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

当使用声明性映射样式进行映射时,主题表格要么是由声明性系统基于__tablename__属性和所呈现的Column对象生成的,要么是通过__table__属性建立的。这两种配置样式分别在具有映射列的声明性表格和具有命令式表格的声明性(又名混合声明性)中呈现。

当使用命令式样式进行映射时,主题表格作为map_imperatively.local_table参数按位置传递。

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

属性字典

这是将与映射类关联的所有属性关联起来的字典。默认情况下,Mapper 从给定的Table生成此字典的条目,形式为每个都引用映射表的单个ColumnColumnProperty对象。属性字典还将包含所有其他需要配置的MapperProperty对象,最常见的是通过relationship()构造函数生成的实例。

当使用声明式映射样式进行映射时,属性字典是由声明式系统通过扫描要映射的类以获取适当属性而生成的。请参阅使用声明式定义映射属性部分以获取有关此过程的说明。

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

其他映射器配置参数

当使用声明式映射样式进行映射时,附加的映射器配置参数通过__mapper_args__类属性进行配置。使用示例请参见使用声明式定义的映射器配置选项。

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

接受的全部参数范围在Mapper中有文档记录。

要映射的类

这是我们应用程序中构建的一个类。通常情况下,此类的结构没有任何限制。[1] 当映射 Python 类时,该类只能有一个Mapper对象。[2]

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

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

表或其他来自子句对象

在绝大多数常见情况下,这是一个Table的实例。对于更高级的用例,它也可能指的是任何类型的FromClause对象,最常见的替代对象是SubqueryJoin对象。

当使用声明式映射风格进行映射时,主题表通过声明系统基于__tablename__属性和提供的Column对象生成,或者通过__table__属性建立。这两种配置样式在使用 mapped_column() 进行声明性表配置和具有命令式表的声明式(也称为混合声明式)中介绍。

当使用命令式风格进行映射时,主题表作为map_imperatively.local_table参数按位置传递。

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

属性字典

这是一个与映射类相关联的所有属性的字典。默认情况下,Mapper从给定的Table中派生此字典的条目,形成每个映射表的ColumnColumnProperty对象。属性字典还将包含要配置的所有其他种类的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__()方法的替代方法,并且允许高度可配置的构造函数形式。

警告

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

包含显式__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()方法进行映射时支持标准配置级别的构造函数。### 在加载过程中保持非映射状态

映射类的__init__()方法在 Python 代码中直接构造对象时被调用:

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),而不是状态,根据需要动态计算属性。

    对于简单的属性,这是最简单且最不容易出错的方法。例如,如果一个名为 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 查询,也可用于 Python 属性。

  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

    table = inspect(User).local_table
    

    对于单表继承映射,其中类是没有自己的表的子类,Mapper.local_table 属性以及 .__table__ 属性将为 None。要检索在查询此类时实际选择的“可选择项”,可通过 Mapper.selectable 属性获得:

    table = inspect(User).selectable
    

Mapper 对象的检查

如前一节所示,Mapper 对象可从任何映射类获得,而不管方法如何,使用 Runtime Inspection 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 #### 映射实例的检查

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

属性状态信息,例如未加载或延迟加载的属性(假设 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 ### 默认构造函数

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__() 方法的类将保持该方法,不会应用默认构造函数。

若要更改使用的默认构造函数,可以提供用户定义的 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,更类似于反序列化,例如取消选中,而不是初始构造。对象的大部分重要状态不是首次组装的,而是重新从数据库行加载的。

因此,为了在对象加载以及构造时保持对象中不是存储到数据库的数据的状态,以下详细介绍了两种一般方法。

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

    对于简单属性,这是最简单且最少错误的方法。例如,如果具有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
    

    如果需要同时使用刷新事件,事件钩子可以叠加在一个可调用对象上,如下所示:

    @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 也将返回:

    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 #### 映射实例的检查

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

属性状态信息,例如尚未加载或延迟加载的属性(假设 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 #### 映射对象的检查

如前一节所示,Mapper对象可以从任何映射类中使用,无论方法如何,都可以使用 Runtime Inspection 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

映射实例的检查

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

posted @ 2024-06-22 11:33  绝不原创的飞龙  阅读(22)  评论(0编辑  收藏  举报