如果您編寫 Web 服務,您很可能會與資料庫互動。有時,您需要進行必須以原子方式應用的更改 - 要么全部成功,要么全部失敗。這就是事務的用武之地。在本文中,我將向您展示如何在程式碼中實現事務,以避免抽象洩漏問題。
一個常見的例子是處理付款:
通常,您的應用程式將有兩個模組來將業務邏輯與資料庫相關程式碼分開。
此模組處理所有與資料庫相關的操作,例如 SQL 查詢。下面,我們定義兩個函數:
import { Injectable } from '@nestjs/common'; import postgres from 'postgres'; @Injectable() export class BillingRepository { constructor( private readonly db_connection: postgres.Sql, ) {} async get_balance(customer_id: string): Promise<number | null> { const rows = await this.db_connection` SELECT amount FROM balances WHERE customer_id=${customer_id} `; return (rows[0]?.amount) ?? null; } async set_balance(customer_id: string, amount: number): Promise<void> { await this.db_connection` UPDATE balances SET amount=${amount} WHERE customer_id=${customer_id} `; } }
服務模組包含業務邏輯,例如取得餘額、驗證餘額以及保存更新後的餘額。
import { Injectable } from '@nestjs/common'; import { BillingRepository } from 'src/billing/billing.repository'; @Injectable() export class BillingService { constructor( private readonly billing_repository: BillingRepository, ) {} async bill_customer(customer_id: string, amount: number) { const balance = await this.billing_repository.get_balance(customer_id); // The balance may change between the time of this check and the update. if (balance === null || balance < amount) { return new Error('Insufficient funds'); } await this.billing_repository.set_balance(customer_id, balance - amount); } }
在 bill_customer 函數中,我們先使用 get_balance 來擷取使用者的餘額。然後,我們檢查餘額是否足夠並使用 set_balance 更新它。
上述程式碼的問題在於,在取得和更新時間之間的餘額可能會改變。為了避免這種情況,我們需要使用交易。您可以透過兩種方式處理這個問題:
相反,我建議採用更乾淨的方法。
處理事務的一個好方法是建立一個在事務中包裝回調的函數。此函數提供了一個會話對象,該對像不會暴露不必要的內部細節,從而防止洩漏抽象。會話物件傳遞給事務中所有與資料庫相關的函數。
實作方法如下:
import { Injectable } from '@nestjs/common'; import postgres, { TransactionSql } from 'postgres'; export type SessionObject = TransactionSql<Record<string, unknown>>; @Injectable() export class BillingRepository { constructor( private readonly db_connection: postgres.Sql, ) {} async run_in_session<T>(cb: (sql: SessionObject) => T | Promise<T>) { return await this.db_connection.begin((session) => cb(session)); } async get_balance( customer_id: string, session: postgres.TransactionSql | postgres.Sql = this.db_connection ): Promise<number | null> { const rows = await session` SELECT amount FROM balances WHERE customer_id=${customer_id} `; return (rows[0]?.amount) ?? null; } async set_balance( customer_id: string, amount: number, session: postgres.TransactionSql | postgres.Sql = this.db_connection ): Promise<void> { await session` UPDATE balances SET amount=${amount} WHERE customer_id=${customer_id} `; } }
在此範例中,run_in_session 函數啟動一個交易並在其中執行回呼。 SessionObject 類型抽象資料庫會話以防止洩漏內部細節。所有與資料庫相關的函數現在都接受會話對象,確保它們可以參與相同事務。
服務模組更新為槓桿交易。它看起來像這樣:
import { Injectable } from '@nestjs/common'; import postgres from 'postgres'; @Injectable() export class BillingRepository { constructor( private readonly db_connection: postgres.Sql, ) {} async get_balance(customer_id: string): Promise<number | null> { const rows = await this.db_connection` SELECT amount FROM balances WHERE customer_id=${customer_id} `; return (rows[0]?.amount) ?? null; } async set_balance(customer_id: string, amount: number): Promise<void> { await this.db_connection` UPDATE balances SET amount=${amount} WHERE customer_id=${customer_id} `; } }
在 bill_customer_transactional 函數中,我們呼叫 run_in_session 並以會話物件作為參數傳遞回調,然後將此參數傳遞給我們呼叫的儲存庫的每個函數。這可確保 get_balance 和 set_balance 在同一事務中運作。如果兩次呼叫之間的餘額發生變化,交易將失敗,從而保持資料完整性。
使用事務有效地確保您的資料庫操作保持一致,尤其是在涉及多個步驟時。我概述的方法可以幫助您在不洩漏抽象的情況下管理事務,從而使您的程式碼更易於維護。嘗試在您的下一個專案中實現此模式,以保持邏輯清晰和資料安全!
感謝您的閱讀!
?喜歡這篇文章別忘了按讚嗎?
聯絡方式
如果您喜歡這篇文章,請隨時在 LinkedIn 上聯繫並在 Twitter 上關注我。
訂閱我的郵件清單:https://sergedevs.com
一定要喜歡並關注嗎?
以上是如何在 TypeScript 中編寫事務資料庫調用的詳細內容。更多資訊請關注PHP中文網其他相關文章!