SqlAlchemy 2.0 中文文档(三)

2024-06-26 14:22:33 浏览数 (1)

原文: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 看起来像:

代码语言:javascript复制
>>> 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表达式:

代码语言:javascript复制
>>> 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()构造来设置绑定参数;这些参数取代了通常放置文本值的位置:

代码语言:javascript复制
>>> 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 语句可以通过使用相关子查询中的其他表中的行来使用。子查询可以用于任何可以放置列表达式的地方:

代码语言:javascript复制
>>> 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对象,以便引用其他表:

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> 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 对象中:

代码语言:javascript复制
>>> 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 如下所示:

代码语言:javascript复制
>>> 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 表达式:

代码语言:javascript复制
>>> 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() 构造来设置绑定参数;这些参数替换了通常放置字面值的位置:

代码语言:javascript复制
>>> 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 语句可以通过使用 相关子查询 来使用其他表中的行。子查询可以在任何可以放置列表达式的地方使用:

代码语言:javascript复制
>>> 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对象,以便引用其他表:

代码语言:javascript复制
>>> 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 子句中时,此语法将隐式生成:

代码语言:javascript复制
>>> 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对象,以便引用其他表:

代码语言:javascript复制
>>> 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]:

代码语言:javascript复制
>>> 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 变体。

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> 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 属性中获取:

代码语言:javascript复制
>>> 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 对象中:

代码语言:javascript复制
>>> 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对象,每个对象代表一个要插入的潜在数据库行:

代码语言:javascript复制
>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

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

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

代码语言:javascript复制
>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

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

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

将对象添加到会话

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

代码语言:javascript复制
>>> session = Session(engine)

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

代码语言:javascript复制
>>> session.add(squidward)
>>> session.add(krabs)

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

代码语言:javascript复制
>>> 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()方法来手动说明刷新过程:

代码语言:javascript复制
>>> 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 属性查看它们:

代码语言:javascript复制
>>> 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:

代码语言:javascript复制
>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

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

代码语言:javascript复制
>>> some_squidward is squidward
True

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

提交

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

代码语言:javascript复制
>>> 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()方法):

代码语言:javascript复制
>>> 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的数据库行:

代码语言:javascript复制
>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')

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

代码语言:javascript复制
>>> sandy.fullname = "Sandy Squirrel"

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

代码语言:javascript复制
>>> sandy in session.dirty
True

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

代码语言:javascript复制
>>> 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 对象现在不再被认为是脏的:

代码语言:javascript复制
>>> sandy in session.dirty
False

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

亦可参见

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

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

代码语言:javascript复制
>>> 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 才会实际发生任何事情:

代码语言:javascript复制
>>> session.delete(patrick)

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

代码语言:javascript复制
>>> 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 中持久存在,这可以通过包含性检查来显示:

代码语言:javascript复制
>>> 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当前关联的所有对象,这将使它们在下次使用时自动刷新,使用一种称为延迟加载的过程:

代码语言:javascript复制
>>> session.rollback()
ROLLBACK

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

代码语言:javascript复制
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

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

代码语言:javascript复制
>>> 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__中:

代码语言:javascript复制
>>> sandy.__dict__  
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

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

代码语言:javascript复制
>>> patrick in session
True

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

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> session.close()
ROLLBACK 

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

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

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

清除Session中的所有对象。

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

代码语言:javascript复制
# 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关联,这将重新建立它们与特定数据库行的关系:

代码语言:javascript复制
>>> 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 对象,每个对象代表一个待插入的潜在数据库行:

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

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

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

添加对象到会话

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

代码语言:javascript复制
>>> session = Session(engine)

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

代码语言:javascript复制
>>> session.add(squidward)
>>> session.add(krabs)

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

代码语言:javascript复制
>>> 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()方法来说明刷新过程:

代码语言:javascript复制
>>> 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属性查看它们:

代码语言:javascript复制
>>> 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:

代码语言:javascript复制
>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

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

代码语言:javascript复制
>>> some_squidward is squidward
True

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

Committing

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

代码语言:javascript复制
>>> session.commit()
COMMIT

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

提示

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

类的实例代表行

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

代码语言:javascript复制
>>> squidward = User(name="squidward", fullname="Squidward Tentacles")
>>> krabs = User(name="ehkrabs", fullname="Eugene H. Krabs")

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

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

代码语言:javascript复制
>>> squidward
User(id=None, name='squidward', fullname='Squidward Tentacles')

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

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

将对象添加到会话

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

代码语言:javascript复制
>>> session = Session(engine)

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

代码语言:javascript复制
>>> session.add(squidward)
>>> session.add(krabs)

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

代码语言:javascript复制
>>> 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()方法手动说明刷新过程:

代码语言:javascript复制
>>> 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 属性查看它们:

代码语言:javascript复制
>>> 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:

代码语言:javascript复制
>>> some_squidward = session.get(User, 4)
>>> some_squidward
User(id=4, name='squidward', fullname='Squidward Tentacles')

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

代码语言:javascript复制
>>> some_squidward is squidward
True

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

提交

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

代码语言:javascript复制
>>> 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()方法):

代码语言:javascript复制
>>> 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的数据库行:

代码语言:javascript复制
>>> sandy
User(id=2, name='sandy', fullname='Sandy Cheeks')

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

代码语言:javascript复制
>>> sandy.fullname = "Sandy Squirrel"

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

代码语言:javascript复制
>>> sandy in session.dirty
True

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

代码语言:javascript复制
>>> 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 对象现在不再被视为脏:

代码语言:javascript复制
>>> sandy in session.dirty
False

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

另见

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

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

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

代码语言:javascript复制
>>> 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要删除,就像其他操作一样,直到刷新进行,实际上什么也不会发生:

代码语言:javascript复制
>>> session.delete(patrick)

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

代码语言:javascript复制
>>> 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 中的持久对象,这可以通过包含检查来展示:

代码语言:javascript复制
>>> 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关联的所有对象过期,这将导致它们在下次使用时自动刷新,这个过程称为惰性加载:

代码语言:javascript复制
>>> session.rollback()
ROLLBACK

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

代码语言:javascript复制
>>> sandy.__dict__
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>}

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

代码语言:javascript复制
>>> 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对象的完整数据库行:

代码语言:javascript复制
>>> sandy.__dict__  
{'_sa_instance_state': <sqlalchemy.orm.state.InstanceState object at 0x...>,
 'id': 2, 'name': 'sandy', 'fullname': 'Sandy Cheeks'}

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

代码语言:javascript复制
>>> patrick in session
True

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

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> session.close()
ROLLBACK 

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

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

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

Session中清除所有对象。

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

代码语言:javascript复制
# 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中,该方法将重新建立它们与特定数据库行的关系:

代码语言:javascript复制
>>> 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() 映射和其他指令。

代码语言:javascript复制
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 列表:

代码语言:javascript复制
>>> 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对象:

代码语言:javascript复制
>>> a1 = Address(email_address="pearl.krabs@gmail.com")
>>> u1.addresses.append(a1)

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

代码语言:javascript复制
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]

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

代码语言:javascript复制
>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')

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

代码语言:javascript复制
>>> 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 属性:

代码语言:javascript复制
# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1
将对象级联到会话中

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

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

代码语言:javascript复制
>>> 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,因为这些对象还没有与真实的数据库行关联:

代码语言:javascript复制
>>> 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列上:

代码语言:javascript复制
>>> 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表中的一组额外行,当我们再次访问这个集合时,我们会再次看到一个延迟加载以检索对象:

代码语言:javascript复制
>>> 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 调用:

代码语言:javascript复制
>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

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

代码语言:javascript复制
>>> 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 子句:

代码语言:javascript复制
>>> 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() 对象:

代码语言:javascript复制
>>> 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 语句关联的对象,例如:

代码语言:javascript复制
for user_obj in session.execute(
    select(User).options(selectinload(User.addresses))
).scalars():
    user_obj.addresses  # access addresses collection already loaded

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

代码语言:javascript复制
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对象:

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> 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 子句中的附加列加载到每个返回对象的相关属性中,例如:

代码语言:javascript复制
>>> 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 查询:

代码语言:javascript复制
>>> 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:

代码语言:javascript复制
>>> 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")

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

代码语言:javascript复制
>>> 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'

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

代码语言:javascript复制
>>> 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 列表:

代码语言:javascript复制
>>> 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 对象:

代码语言:javascript复制
>>> a1 = Address(email_address="pearl.krabs@gmail.com")
>>> u1.addresses.append(a1)

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

代码语言:javascript复制
>>> u1.addresses
[Address(id=None, email_address='pearl.krabs@gmail.com')]

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

代码语言:javascript复制
>>> a1.user
User(id=None, name='pkrabs', fullname='Pearl Krabs')

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

代码语言:javascript复制
>>> 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属性进行赋值:

代码语言:javascript复制
# equivalent effect as a2 = Address(user=u1)
>>> a2.user = u1
将对象级联到会话中

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

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

代码语言:javascript复制
>>> 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,因为这些对象尚未与真实的数据库行关联:

代码语言:javascript复制
>>> 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 列中:

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> 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 列上:

代码语言:javascript复制
>>> 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 对象的新生成的主键时:

代码语言:javascript复制
>>> 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 表的一组额外行,因此当我们再次访问此集合时,我们会再次看到一个懒加载以检索对象:

代码语言:javascript复制
>>> 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 调用:

代码语言:javascript复制
>>> u1.addresses
[Address(id=4, email_address='pearl.krabs@gmail.com'), Address(id=5, email_address='pearl@aol.com')]

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

代码语言:javascript复制
>>> 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 子句:

代码语言:javascript复制
>>> 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() 对象:

代码语言:javascript复制
>>> 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 子句:

代码语言:javascript复制
>>> 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() 对象:

代码语言:javascript复制
>>> 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 语句关联,例如:

代码语言:javascript复制
for user_obj in session.execute(
    select(User).options(selectinload(User.addresses))
).scalars():
    user_obj.addresses  # access addresses collection already loaded

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

代码语言:javascript复制
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 对象:

代码语言:javascript复制
>>> 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 的情况下使用内连接而不是外连接:

代码语言:javascript复制
>>> 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 子句中的附加列加载到每个返回对象的相关属性中,例如:

代码语言:javascript复制
>>> 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 查询:

代码语言:javascript复制
>>> 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:

代码语言:javascript复制
>>> 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")

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

代码语言:javascript复制
>>> 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'

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

代码语言:javascript复制
>>> 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 对象:

代码语言:javascript复制
>>> 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

代码语言:javascript复制
>>> 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 子句中的其他列加载到每个返回对象的相关属性中,例如:

代码语言:javascript复制
>>> 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 查询:

代码语言:javascript复制
>>> 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:

代码语言:javascript复制
>>> 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")

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

代码语言:javascript复制
>>> 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'

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

代码语言:javascript复制
>>> 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 防止不必要的惰性加载 - 在 关系加载技术 中 (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

代码语言:javascript复制
另请参阅

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

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

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

### Raiseload

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

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

```py
>>> 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")

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

代码语言:javascript复制
>>> 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'

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

代码语言:javascript复制
>>> 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 防止不必要的惰性加载 - 在 关系加载技术 中

0 人点赞