SqlAlchemy-2-0-中文文件-二-

绝不原创的飞龙發表於2024-06-22

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

相關文章