首頁  >  文章  >  web前端  >  如何在 TypeScript 中編寫事務資料庫調用

如何在 TypeScript 中編寫事務資料庫調用

DDD
DDD原創
2024-11-06 19:25:031043瀏覽

How To Write Transactional Database Calls in TypeScript

如果您編寫 Web 服務,您很可能會與資料庫互動。有時,您需要進行必須以原子方式應用的更改 - 要么全部成功,要么全部失敗。這就是事務的用武之地。在本文中,我將向您展示如何在程式碼中實現事務,以避免抽象洩漏問題。

例子

一個常見的例子是處理付款:

  • 您需要取得使用者的餘額並檢查是否足夠。
  • 然後,您更新餘額並保存。

結構

通常,您的應用程式將有兩個模組來將業務邏輯與資料庫相關程式碼分開。

儲存庫模組

此模組處理所有與資料庫相關的操作,例如 SQL 查詢。下面,我們定義兩個函數:

  • get_balance — 從資料庫中檢索使用者的餘額。
  • set_balance — 更新使用者的餘額。
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中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn