資料庫交易

Django 提供幾種方式來控制資料庫交易的管理方式。

管理資料庫交易

Django 的預設交易行為

Django 的預設行為是在自動提交模式下執行。每個查詢都會立即提交到資料庫,除非有交易正在進行。 請參閱下方以了解詳細資訊

Django 會自動使用交易或儲存點來保證需要多個查詢的 ORM 操作的完整性,尤其是 delete()update() 查詢。

為了效能考量,Django 的 TestCase 類別也會將每個測試包裝在交易中。

將交易綁定到 HTTP 請求

在 Web 上處理交易的常見方法是將每個請求包裝在交易中。將 ATOMIC_REQUESTS 設定為 True,針對您想要啟用此行為的每個資料庫進行設定。

它的運作方式如下。在呼叫檢視函數之前,Django 會啟動交易。如果回應產生時沒有問題,Django 會提交交易。如果檢視產生例外,Django 會回滾交易。

您可以在檢視程式碼中使用儲存點執行子交易,通常是使用 atomic() 內容管理器。但是,在檢視結束時,所有或任何變更都會被提交。

警告

雖然此交易模型的簡單性很吸引人,但當流量增加時,它也會變得效率低下。為每個檢視開啟交易會有一些額外負荷。對效能的影響取決於應用程式的查詢模式以及資料庫處理鎖定的程度。

每個請求的交易和串流回應

當檢視傳回 StreamingHttpResponse 時,讀取回應的內容通常會執行程式碼來產生內容。由於檢視已傳回,因此此類程式碼會在交易之外執行。

一般而言,不建議在產生串流回應時寫入資料庫,因為在開始傳送回應後,沒有合理的方法來處理錯誤。

實際上,此功能會將每個檢視函數包裝在下面描述的 atomic() 修飾器中。

請注意,只有您檢視的執行會包含在交易中。中介軟體會在交易之外執行,範本回應的呈現也是如此。

當啟用 ATOMIC_REQUESTS 時,仍然可以防止檢視在交易中執行。

non_atomic_requests(using=None)[來源]

此修飾器將否定 ATOMIC_REQUESTS 對於給定檢視的效果

from django.db import transaction


@transaction.non_atomic_requests
def my_view(request):
    do_stuff()


@transaction.non_atomic_requests(using="other")
def my_other_view(request):
    do_stuff_on_the_other_database()

它只有在套用至檢視本身時才有效。

明確控制交易

Django 提供單一 API 來控制資料庫交易。

atomic(using=None, savepoint=True, durable=False)[來源]

原子性是資料庫交易的定義屬性。 atomic 讓我們可以建立一個程式碼區塊,在其中保證資料庫上的原子性。如果程式碼區塊成功完成,則變更會提交到資料庫。如果有例外,則變更會回滾。

atomic 區塊可以巢狀。在這種情況下,當內部區塊成功完成時,如果稍後在外部區塊中引發例外,其效果仍然可以回滾。

有時,確保 atomic 區塊始終是最外層的 atomic 區塊會很有用,以確保當區塊在沒有錯誤的情況下退出時,會提交任何資料庫變更。這稱為持久性,可以透過設定 durable=True 來實現。如果 atomic 區塊巢狀在另一個區塊中,則會引發 RuntimeError

atomic 可以作為 修飾器 使用

from django.db import transaction


@transaction.atomic
def viewfunc(request):
    # This code executes inside a transaction.
    do_stuff()

也可以作為 內容管理器 使用

from django.db import transaction


def viewfunc(request):
    # This code executes in autocommit mode (Django's default).
    do_stuff()

    with transaction.atomic():
        # This code executes inside a transaction.
        do_more_stuff()

atomic 包裝在 try/except 區塊中允許自然地處理完整性錯誤

from django.db import IntegrityError, transaction


@transaction.atomic
def viewfunc(request):
    create_parent()

    try:
        with transaction.atomic():
            generate_relationships()
    except IntegrityError:
        handle_exception()

    add_children()

在此範例中,即使 generate_relationships() 因破壞完整性限制而導致資料庫錯誤,您也可以在 add_children() 中執行查詢,並且來自 create_parent() 的變更仍然存在,並且綁定到相同的交易。請注意,當呼叫 handle_exception() 時,generate_relationships() 中嘗試的任何操作都將已安全地回滾,因此,必要時例外處理常式也可以在資料庫上運作。

避免在 atomic 內捕捉例外!

當退出 atomic 區塊時,Django 會查看它是正常退出還是有例外,以決定是否提交或回滾。如果您在 atomic 區塊內捕捉並處理例外,您可能會向 Django 隱藏已發生問題的事實。這可能會導致意外的行為。

這主要是 DatabaseError 及其子類別(例如 IntegrityError)的考量。發生此類錯誤後,交易會中斷,Django 會在 atomic 區塊結束時執行回滾。如果您嘗試在回滾發生之前執行資料庫查詢,Django 會引發 TransactionManagementError。當與 ORM 相關的訊號處理常式引發例外時,您也可能會遇到此行為。

捕捉資料庫錯誤的正確方法是在如上所示的 atomic 區塊周圍。如有必要,請為此目的新增額外的 atomic 區塊。此模式還有另一個優點:它可以明確劃定如果發生例外將會回滾哪些操作。

如果您捕捉原始 SQL 查詢引發的例外,Django 的行為是未指定的,並且取決於資料庫。

當回滾交易時,您可能需要手動還原應用程式狀態。

當發生交易回滾時,模型的欄位值不會還原。除非您手動還原原始欄位值,否則可能會導致模型狀態不一致。

例如,給定具有 active 欄位的 MyModel,此程式碼片段可確保如果將 active 更新為 True 在交易中失敗,則結尾的 if obj.active 檢查會使用正確的值

from django.db import DatabaseError, transaction

obj = MyModel(active=False)
obj.active = True
try:
    with transaction.atomic():
        obj.save()
except DatabaseError:
    obj.active = False

if obj.active:
    ...

這也適用於可能保存應用程式狀態的任何其他機制,例如快取或全域變數。例如,如果程式碼在儲存物件後主動更新快取中的資料,建議改為使用 transaction.on_commit(),以將快取變更延遲到實際提交交易時。

為了確保原子性,atomic 會停用某些 API。在 atomic 區塊內嘗試提交 (commit)、回滾 (roll back) 或變更資料庫連線的自動提交狀態,都會引發例外。

atomic 接受一個 using 參數,該參數應該是資料庫的名稱。如果沒有提供此參數,Django 會使用 "default" 資料庫。

在底層,Django 的交易管理程式碼會:

  • 在進入最外層的 atomic 區塊時開啟一個交易;

  • 在進入內層的 atomic 區塊時建立一個儲存點 (savepoint);

  • 在退出內層區塊時釋放或回滾到儲存點;

  • 在退出最外層區塊時提交或回滾交易。

您可以透過將 savepoint 參數設定為 False 來停用內層區塊儲存點的建立。如果發生例外,Django 會在退出第一個具有儲存點的父區塊時執行回滾(如果有),否則會在退出最外層區塊時執行回滾。原子性仍然由外部交易保證。只有在儲存點的開銷明顯時才應使用此選項。它會帶來破壞上述錯誤處理的缺點。

您可以在關閉自動提交時使用 atomic。即使是最外層的區塊,它也只會使用儲存點。

效能考量

開啟的交易會對您的資料庫伺服器造成效能成本。為了將此開銷降到最低,請盡可能縮短交易時間。如果您在 Django 的請求/回應週期之外的長時間執行程序中使用 atomic(),這一點尤其重要。

自動提交

為什麼 Django 使用自動提交

在 SQL 標準中,每個 SQL 查詢都會啟動一個交易,除非已經有交易在作用中。這些交易必須明確地提交或回滾。

對於應用程式開發人員來說,這並不總是方便的。為了緩解這個問題,大多數資料庫都提供了自動提交模式。當自動提交開啟且沒有交易在作用中時,每個 SQL 查詢都會被包裝在它自己的交易中。換句話說,每個這樣的查詢不僅會啟動一個交易,而且該交易也會根據查詢是否成功而自動提交或回滾。

PEP 249,Python 資料庫 API 規範 v2.0,要求最初關閉自動提交。Django 覆寫了這個預設值並開啟了自動提交。

為了避免這種情況,您可以停用交易管理,但不建議這樣做。

停用交易管理

您可以透過在其設定中將 AUTOCOMMIT 設定為 False 來完全停用 Django 對於給定資料庫的交易管理。如果您這樣做,Django 將不會啟用自動提交,也不會執行任何提交。您將獲得底層資料庫函式庫的常規行為。

這要求您明確提交每個交易,即使是 Django 或第三方函式庫啟動的交易。因此,這最適合用於您想要執行自己的交易控制中間件或執行真正奇怪的事情的情況。

在提交後執行動作

有時您需要執行與目前資料庫交易相關的動作,但前提是交易成功提交。例如,後台工作、電子郵件通知或快取失效。

on_commit() 允許您註冊回呼函數,這些回呼函數會在開放交易成功提交後執行。

on_commit(func, using=None, robust=False)[原始碼]

將函數或任何可呼叫物件傳遞給 on_commit()

from django.db import transaction


def send_welcome_email(): ...


transaction.on_commit(send_welcome_email)

回呼函數不會傳遞任何引數,但您可以使用 functools.partial() 來綁定它們。

from functools import partial

for user in users:
    transaction.on_commit(partial(send_invite_email, user=user))

回呼函數會在開放交易成功提交後呼叫。如果交易改為回滾(通常是在 atomic() 區塊中引發未處理的例外時),則回呼函數會被捨棄,並且永遠不會被呼叫。

如果您在沒有開啟交易的情況下呼叫 on_commit(),則回呼函數會立即執行。

有時註冊可能會失敗的回呼函數很有用。傳遞 robust=True 允許即使目前的回呼函數拋出例外,也可以執行下一個回呼函數。所有源自 Python Exception 類別的錯誤都會被捕獲並記錄到 django.db.backends.base 記錄器。

您可以使用 TestCase.captureOnCommitCallbacks() 來測試使用 on_commit() 註冊的回呼函數。

儲存點

儲存點(即巢狀 atomic() 區塊)會被正確處理。也就是說,在儲存點之後(在巢狀 atomic() 區塊中)註冊的 on_commit() 可呼叫物件將在外部交易提交後呼叫,但如果交易期間發生回滾到該儲存點或任何先前儲存點的情況,則不會呼叫。

with transaction.atomic():  # Outer atomic, start a new transaction
    transaction.on_commit(foo)

    with transaction.atomic():  # Inner atomic block, create a savepoint
        transaction.on_commit(bar)

# foo() and then bar() will be called when leaving the outermost block

另一方面,當儲存點回滾時(由於引發例外),內部可呼叫物件將不會被呼叫。

with transaction.atomic():  # Outer atomic, start a new transaction
    transaction.on_commit(foo)

    try:
        with transaction.atomic():  # Inner atomic block, create a savepoint
            transaction.on_commit(bar)
            raise SomeError()  # Raising an exception - abort the savepoint
    except SomeError:
        pass

# foo() will be called, but not bar()

執行順序

給定交易的提交時函數會按照註冊的順序執行。

例外處理

如果給定交易中以 robust=False 註冊的一個提交時函數引發未捕獲的例外,則在同一交易中稍後註冊的函數將不會執行。這與您在沒有 on_commit() 的情況下依序執行函數的行為相同。

執行時機

您的回呼函數會在成功提交執行,因此回呼函數中的失敗不會導致交易回滾。它們會根據交易的成功與否有條件地執行,但它們不是交易的一部分。對於預期的用例(郵件通知、後台工作等),這應該沒問題。如果不是(如果您的後續動作非常關鍵,以至於它的失敗應該意味著交易本身的失敗),那麼您不想使用 on_commit() 掛鉤。相反,您可能需要兩階段提交,例如 psycopg 兩階段提交協定支援Python DB-API 規範中的可選兩階段提交擴展

回呼函數只有在提交後連線上恢復自動提交時才會執行(因為否則在回呼函數中完成的任何查詢都會開啟一個隱式交易,阻止連線返回自動提交模式)。

在自動提交模式下且在 atomic() 區塊之外時,該函數會立即執行,而不是在提交時執行。

提交時函數僅適用於自動提交模式atomic()(或 ATOMIC_REQUESTS)交易 API。當自動提交停用且您不在原子區塊內時呼叫 on_commit() 將會導致錯誤。

在測試中使用

Django 的 TestCase 類別會將每個測試包裝在一個交易中,並在每個測試後回滾該交易,以提供測試隔離。這表示永遠不會實際提交任何交易,因此您的 on_commit() 回呼函數永遠不會執行。

您可以使用 TestCase.captureOnCommitCallbacks() 來克服此限制。這會將您的 on_commit() 回呼函數捕獲到列表中,讓您可以對它們進行斷言,或透過呼叫它們來模擬交易提交。

另一種克服限制的方法是使用 TransactionTestCase 而不是 TestCase。這表示您的交易會被提交,並且回呼函數將會執行。然而,TransactionTestCase 會在測試之間清除資料庫,這比 TestCase 的隔離機制慢很多。

為什麼沒有回滾 hook?

回滾 hook 比提交 hook 更難以穩健地實作,因為很多事情都可能導致隱含的回滾。

例如,如果您的資料庫連線因為您的程序在沒有機會優雅關閉的情況下被終止而斷開,您的回滾 hook 將永遠不會執行。

但有一個解決方案:與其在原子區塊(交易)期間執行某些操作,然後在交易失敗時撤銷它,不如使用 on_commit() 來延遲執行,直到交易成功之後。從一開始就不做某事比撤銷已經做過的事容易得多!

底層 API

警告

如果可以的話,請盡可能優先使用 atomic()。它會考慮每個資料庫的特性,並防止無效的操作。

只有在您實作自己的交易管理時,底層 API 才有用。

自動提交

Django 在 django.db.transaction 模組中提供了一個 API 來管理每個資料庫連線的自動提交狀態。

get_autocommit(using=None)[原始碼]
set_autocommit(autocommit, using=None)[原始碼]

這些函數接受一個 using 引數,它應該是資料庫的名稱。如果沒有提供,Django 會使用 "default" 資料庫。

自動提交最初是開啟的。如果您關閉它,您有責任恢復它。

一旦您關閉自動提交,您就會獲得資料庫轉接器的預設行為,而 Django 不會提供任何幫助。儘管該行為在 PEP 249 中指定,但轉接器的實作並不總是彼此一致。請仔細閱讀您正在使用的轉接器的文件。

您必須確保沒有活動的交易,通常是透過發出 commit()rollback(),然後再重新開啟自動提交。

atomic() 區塊處於活動狀態時,Django 將拒絕關閉自動提交,因為這會破壞原子性。

交易

交易是一組原子性的資料庫查詢。即使您的程式崩潰,資料庫也會保證所有變更都會被套用,或者一個都不會。

Django 不提供啟動交易的 API。啟動交易的預期方法是使用 set_autocommit() 停用自動提交。

一旦您進入交易,您可以選擇使用 commit() 套用您到目前為止執行的變更,或使用 rollback() 取消它們。這些函數定義在 django.db.transaction 中。

commit(using=None)[原始碼]
rollback(using=None)[原始碼]

這些函數接受一個 using 引數,它應該是資料庫的名稱。如果沒有提供,Django 會使用 "default" 資料庫。

atomic() 區塊處於活動狀態時,Django 將拒絕提交或回滾,因為這會破壞原子性。

儲存點

儲存點是交易中的一個標記,可讓您回滾部分交易,而不是整個交易。儲存點適用於 SQLite、PostgreSQL、Oracle 和 MySQL(使用 InnoDB 儲存引擎時)後端。其他後端提供儲存點函數,但它們是空操作 – 它們實際上不做任何事情。

如果您使用自動提交(Django 的預設行為),儲存點不是特別有用。但是,一旦您使用 atomic() 開啟交易,您就會建立一系列等待提交或回滾的資料庫操作。如果您發出回滾,整個交易都會回滾。儲存點提供執行細粒度回滾的能力,而不是 transaction.rollback() 將執行的完整回滾。

atomic() 裝飾器巢狀時,它會建立一個儲存點以允許部分提交或回滾。強烈建議您使用 atomic() 而不是下面描述的函數,但它們仍然是公用 API 的一部分,並且沒有計劃棄用它們。

這些函數的每一個都接受一個 using 引數,它應該是套用該行為的資料庫的名稱。如果沒有提供 using 引數,則會使用 "default" 資料庫。

儲存點由 django.db.transaction 中的三個函數控制

savepoint(using=None)[原始碼]

建立一個新的儲存點。這會標記交易中已知處於「良好」狀態的點。傳回儲存點 ID ( sid )。

savepoint_commit(sid, using=None)[原始碼]

釋放儲存點 sid。自建立儲存點以來執行的變更會成為交易的一部分。

savepoint_rollback(sid, using=None)[原始碼]

將交易回滾到儲存點 sid

如果儲存點不受支援或資料庫處於自動提交模式,這些函數將不會執行任何操作。

此外,還有一個實用函數

clean_savepoints(using=None)[原始碼]

重設用於產生唯一儲存點 ID 的計數器。

以下範例示範了儲存點的使用

from django.db import transaction


# open a transaction
@transaction.atomic
def viewfunc(request):
    a.save()
    # transaction now contains a.save()

    sid = transaction.savepoint()

    b.save()
    # transaction now contains a.save() and b.save()

    if want_to_keep_b:
        transaction.savepoint_commit(sid)
        # open transaction still contains a.save() and b.save()
    else:
        transaction.savepoint_rollback(sid)
        # open transaction now contains only a.save()

儲存點可用於透過執行部分回滾來從資料庫錯誤中恢復。如果您在 atomic() 區塊內執行此操作,則整個區塊仍將被回滾,因為它不知道您已在較低層級處理了情況!為了防止這種情況,您可以使用以下函數來控制回滾行為。

get_rollback(using=None)[原始碼]
set_rollback(rollback, using=None)[原始碼]

將回滾標誌設為 True 會強制在退出最內層的原子區塊時進行回滾。這在不引發異常的情況下觸發回滾時可能很有用。

將其設為 False 會阻止此類回滾。在執行此操作之前,請確保您已將交易回滾到目前原子區塊內已知的良好儲存點!否則,您將破壞原子性,並可能發生資料損毀。

特定資料庫注意事項

SQLite 中的儲存點

雖然 SQLite 支援儲存點,但 sqlite3 模組的設計存在缺陷,導致它們幾乎無法使用。

當自動提交啟用時,儲存點沒有意義。當它被停用時,sqlite3 會在儲存點陳述式之前隱式提交。(事實上,它會在除了 SELECTINSERTUPDATEDELETEREPLACE 之外的任何陳述式之前提交。)此錯誤有兩個後果

  • 儲存點的低階 API 只能在交易內部使用,也就是在 atomic() 區塊內使用。

  • 當自動提交關閉時,無法使用 atomic()

MySQL 中的交易

如果您正在使用 MySQL,您的資料表可能支援或可能不支援交易;這取決於您的 MySQL 版本和您正在使用的資料表類型。(「資料表類型」是指類似「InnoDB」或「MyISAM」之類的東西。)MySQL 交易的特殊性不在本文的討論範圍內,但 MySQL 網站上有關於 MySQL 交易的資訊

如果您的 MySQL 設定支援交易,則 Django 將始終在自動提交模式下運行:陳述式將在調用後立即執行並提交。如果您的 MySQL 設定確實支援交易,Django 將按照本文檔中的說明處理交易。

處理 PostgreSQL 交易中的例外

注意

只有當您實作自己的交易管理時,此部分才相關。此問題不會在 Django 的預設模式中發生,而 atomic() 會自動處理此問題。

在交易中,當對 PostgreSQL 資料指標的調用引發例外(通常是 IntegrityError)時,同一交易中的所有後續 SQL 都將失敗,並顯示錯誤「目前交易已中止,在交易區塊結束之前忽略查詢」。雖然基本使用 save() 不太可能在 PostgreSQL 中引發例外,但還有更多進階的使用模式可能會引發例外,例如儲存具有唯一欄位的物件、使用 force_insert/force_update 標誌進行儲存,或調用自訂 SQL。

有幾種方法可以從這類錯誤中恢復。

交易回滾

第一個選項是回滾整個交易。例如

a.save()  # Succeeds, but may be undone by transaction rollback
try:
    b.save()  # Could throw exception
except IntegrityError:
    transaction.rollback()
c.save()  # Succeeds, but a.save() may have been undone

調用 transaction.rollback() 會回滾整個交易。任何未提交的資料庫操作都將遺失。在此範例中,即使 a.save() 操作本身沒有引發任何錯誤,其所做的變更也會遺失。

儲存點回滾

您可以使用 儲存點 來控制回滾的範圍。在執行可能失敗的資料庫操作之前,您可以設定或更新儲存點;這樣,如果操作失敗,您可以回滾單個有問題的操作,而不是整個交易。例如

a.save()  # Succeeds, and never undone by savepoint rollback
sid = transaction.savepoint()
try:
    b.save()  # Could throw exception
    transaction.savepoint_commit(sid)
except IntegrityError:
    transaction.savepoint_rollback(sid)
c.save()  # Succeeds, and a.save() is never undone

在此範例中,如果 b.save() 引發例外,a.save() 將不會被復原。

返回頂部