首頁 >後端開發 >Golang >使用通用框架在 Go 中建立健全的 SQL 事務執行

使用通用框架在 Go 中建立健全的 SQL 事務執行

DDD
DDD原創
2024-12-11 10:04:10701瀏覽

Building Robust SQL Transaction Execution in Go with a Generic Framework

在 Go 中使用 SQL 資料庫時,確保原子性並在多步驟事務期間管理回滾可能具有挑戰性。在本文中,我將指導您建立一個健全、可重複使用且可測試的框架,用於在 Go 中執行 SQL 事務,並使用泛型來實現靈活性。

我們將建立一個 SqlWriteExec 實用程序,用於在事務中執行多個相關資料庫操作。它支援無狀態和有狀態操作,支援複雜的工作流程,例如插入相關實體,同時無縫管理依賴關係。

為什麼我們需要 SQL 交易框架?

在實際應用中,資料庫操作很少是孤立的。考慮以下場景:

插入使用者並自動更新其庫存。
建立訂單並處理付款,確保一致性。
由於涉及多個步驟,在故障期間管理回滾對於確保資料完整性至關重要。

在 Txn 管理中使用 go。

如果您正在編寫資料庫交易,那麼在編寫核心邏輯之前您可能需要考慮幾個樣板。雖然這個 txn 管理是由 java 中的 spring boot 管理的,並且在用 java 編寫程式碼時你從來沒有太在意這些,但在 golang 中卻不是這樣。下面提供了一個簡單的範例

func basicTxn(db *sql.DB) error {
    // start a transaction
    tx, err := db.Begin()
    if err != nil {
        return err
    }
    defer func() {
        if r := recover(); r != nil {
            tx.Rollback()
        } else if err != nil {
            tx.Rollback()
        } else {
            tx.Commit()
        }
    }()

    // insert data into the orders table
    _, err = tx.Exec("INSERT INTO orders (id, customer_name, order_date) VALUES (1, 'John Doe', '2022-01-01')")
    if err != nil {
        return err
    }
    return nil
}

我們不能指望為每個函數重複回滾/提交程式碼。這裡我們有兩個選擇,要么創建一個類,該類將提供一個函數作為返回類型,該類在defer 中執行時將提交/回滾txn,要么創建一個包裝類,它將所有txn 函數包裝在一起並一次性執行。

我選擇了後面的選擇,程式碼的變更如下所示。

func TestSqlWriteExec_CreateOrderTxn(t *testing.T) {

    db := setupDatabase()
    // create a new SQL Write Executor
    err := dbutils.NewSqlTxnExec[OrderRequest, OrderProcessingResponse](context.TODO(), db, nil, &OrderRequest{CustomerName: "CustomerA", ProductID: 1, Quantity: 10}).
        StatefulExec(InsertOrder).
        StatefulExec(UpdateInventory).
        StatefulExec(InsertShipment).
        Commit()
    // check if the transaction was committed successfully
    if err != nil {
        t.Fatal(err)
        return
    }
    verifyTransactionSuccessful(t, db)
    t.Cleanup(
        func() { 
            cleanup(db)
            db.Close() 
        },
    )
}
func InsertOrder(ctx context.Context, txn *sql.Tx, order *OrderRequest, orderProcessing *OrderProcessingResponse) error {
    // Insert Order
    result, err := txn.Exec("INSERT INTO orders (customer_name, product_id, quantity) VALUES (, , )", order.CustomerName, order.ProductID, order.Quantity)
    if err != nil {
        return err
    }
    // Get the inserted Order ID
    orderProcessing.OrderID, err = result.LastInsertId()
    return err
}

func UpdateInventory(ctx context.Context, txn *sql.Tx, order *OrderRequest, orderProcessing *OrderProcessingResponse) error {
    // Update Inventory if it exists and the quantity is greater than the quantity check if it exists
    result, err := txn.Exec("UPDATE inventory SET product_quantity = product_quantity -  WHERE id =  AND product_quantity >= ", order.Quantity, order.ProductID)
    if err != nil {
        return err
    }
    // Get the number of rows affected
    rowsAffected, err := result.RowsAffected()
    if rowsAffected == 0 {
        return errors.New("Insufficient inventory")
    }
    return err
}

func InsertShipment(ctx context.Context, txn *sql.Tx, order *OrderRequest, orderProcessing *OrderProcessingResponse) error {
    // Insert Shipment
    result, err := txn.Exec("INSERT INTO shipping_info (customer_name, shipping_address) VALUES (, 'Shipping Address')", order.CustomerName)
    if err != nil {
        return err
    }
    // Get the inserted Shipping ID
    orderProcessing.ShippingID, err = result.LastInsertId()
    return err
}

這段程式碼將會更加精確和簡潔。

核心邏輯是如何實現的

這個想法是將 txn 隔離到單一 go 結構,以便它可以接受多個 txn。我所說的 txn 是指將使用我們為類別創建的 txn 執行操作的函數。

type TxnFn[T any] func(ctx context.Context, txn *sql.Tx, processingReq *T) error
type StatefulTxnFn[T any, R any] func(ctx context.Context, txn *sql.Tx, processingReq *T, processedRes *R) error

這兩個是函數類型,它們將接受 txn 來處理某些內容。現在在資料層中實作建立一個像這樣的函數並將其傳遞給執行器類,該執行器類別負責注入參數並執行該函數。

// SQL Write Executor is responsible when executing write operations
// For dependent writes you may need to add the dependent data to processReq and proceed to the next function call
type SqlTxnExec[T any, R any] struct {
    db               *sql.DB
    txn              *sql.Tx
    txnFns         []TxnFn[T]
    statefulTxnFns []StatefulTxnFn[T, R]
    processingReq    *T
    processedRes     *R
    ctx              context.Context
    err              error
}

這是我們儲存所有 txn_fn 詳細資訊的地方,我們將使用 Commit() 方法來嘗試提交 txn。

func (s *SqlTxnExec[T, R]) Commit() (err error) {
    defer func() {
        if p := recover(); p != nil {
            s.txn.Rollback()
            panic(p)
        } else if err != nil {
            err = errors.Join(err, s.txn.Rollback())
        } else {
            err = errors.Join(err, s.txn.Commit())
        }
        return
    }()

    for _, writeFn := range s.txnFns {
        if err = writeFn(s.ctx, s.txn, s.processingReq); err != nil {
            return
        }
    }

    for _, statefulWriteFn := range s.statefulTxnFns {
        if err = statefulWriteFn(s.ctx, s.txn, s.processingReq, s.processedRes); err != nil {
            return
        }
    }
    return
}

您可以在儲存庫中找到更多範例和測試 -
https://github.com/mahadev-k/go-utils/tree/main/examples

雖然現在我們偏向分散式系統和共識協議,但我們仍然使用sql並且它仍然存在。

如果有人願意貢獻並在此基礎上繼續發展,請告訴我! !
感謝您閱讀本文! !
https://in.linkedin.com/in/mahadev-k-934520223
https://x.com/mahadev_k_

以上是使用通用框架在 Go 中建立健全的 SQL 事務執行的詳細內容。更多資訊請關注PHP中文網其他相關文章!

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