Go で SQL データベースを操作する場合、アトミック性を確保し、複数ステップのトランザクション中にロールバックを管理することが困難になることがあります。この記事では、柔軟なジェネリックスを使用して、Go で SQL トランザクションを実行するための堅牢で再利用可能でテスト可能なフレームワークを作成する方法を説明します。
トランザクション内で複数の依存データベース操作を実行するための SqlWriteExec ユーティリティを構築します。ステートレス操作とステートフル操作の両方をサポートし、依存関係をシームレスに管理しながら関連エンティティを挿入するなどの高度なワークフローを可能にします。
実際のアプリケーションでは、データベース操作が分離されることはほとんどありません。次のシナリオを検討してください:
ユーザーを挿入し、そのインベントリをアトミックに更新します。
注文の作成と支払いの処理により、一貫性が確保されます。
複数の手順が関係するため、データの整合性を確保するには障害時のロールバックの管理が重要になります。
データベース TXN を作成している場合は、コア ロジックを作成する前に考慮する必要があるいくつかのボイラー プレートが存在する可能性があります。この 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 }
すべての関数に対してロールバック/コミット コードを繰り返すことは期待できません。ここには 2 つのオプションがあります。遅延で実行されるときに 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 struct に分離して、複数の 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
これら 2 つは、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 の詳細を保存する場所であり、txn のコミットを試行する Commit() メソッドがあります。
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 を使用しており、SQL はまだ存在しています。
誰かがこれに貢献してこれを構築したい場合はお知らせください!!
ここまで読んでいただきありがとうございます!!
https://in.linkedin.com/in/mahadev-k-934520223
https://x.com/mahadev_k_
以上が汎用フレームワークを使用した Go での堅牢な SQL トランザクション実行の構築の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。