Ao trabalhar com bancos de dados SQL no GO, garantindo atomicidade e gerenciando reversão durante transações em várias etapas pode ser um desafio. Neste artigo, vou guiá -lo através da criação de uma estrutura robusta, reutilizável e testável para executar transações SQL no GO, usando genéricos para flexibilidade.
criaremos um utilitário SQLWRITEEXEC para executar várias operações de banco de dados dependentes em uma transação. Ele suporta operações sem estado e com estado, permitindo fluxos de trabalho sofisticados, como inserir entidades relacionadas enquanto gerencia dependências sem problemas.
Em aplicativos do mundo real, as operações do banco de dados raramente são isoladas. Considere estes cenários:
inserindo um usuário e atualizando seu inventário atomicamente.
Criando um pedido e processando seu pagamento, garantindo consistência.
Com várias etapas envolvidas, o gerenciamento de reversões durante as falhas se torna crucial para garantir a integridade dos dados.
Se você estiver escrevendo um banco de dados TXN, pode haver várias placas de caldeira que você pode precisar considerar antes de escrever a lógica principal. Embora esse gerenciamento do TXN seja gerenciado pela Spring Boot em Java e você nunca se incomodasse muito com isso enquanto escreve código em Java, mas esse não é o caso em Golang. Um exemplo simples é fornecido abaixo
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 }
Não podemos esperar repetir o código de reversão/compromisso para todas as funções. Temos duas opções aqui, crie uma classe que fornecerá uma função como um tipo de retorno que, quando executado no adiamento, comprometerá/reverterá o TXN ou criará uma classe de wrapper que envolverá todos os funcionários do TXN e executará de uma só vez.
fui com a escolha posterior e a mudança no código pode ser vista abaixo.
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 ($1, $2, $3)", 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 - $1 WHERE id = $2 AND product_quantity >= $1", 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 ($1, 'Shipping Address')", order.CustomerName) if err != nil { return err } // Get the inserted Shipping ID orderProcessing.ShippingID, err = result.LastInsertId() return err }
Este código será muito mais preciso e conciso.
A idéia é isolar o TXN em uma única estrutura Go, de modo que ele possa aceitar vários TXNs. Por txn quero dizer funções que farão ação com o TXN que criamos para a classe.
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
esses dois são tipos de função que levarão um TXN para processar algo. Agora, na camada de dados, implementando uma função Crie uma função como essa e passe -a para a classe Executor, que cuida de injetar os args e executar a função.
// 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 }
é aqui que armazenamos todos os detalhes txn_fn e teremos o método Commit () para tentar cometer o 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 }
você pode encontrar mais exemplos e testes no repo -
https://github.com/mahadev-k/go-utils/tree/main/examples/
!
Obrigado por ler até agora !!
https://in.linkedin.com/in/mahadev-k-934520223
https://x.com/mahadev_k_
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3