cuando se trabaja con bases de datos SQL en GO, asegurando la atomicidad y la gestión de reversiones durante las transacciones de múltiples pasos puede ser un desafío. En este artículo, lo guiaré a través de la creación de un marco robusto, reutilizable y comprobable para ejecutar transacciones SQL en GO, usando genéricos para flexibilidad.
construiremos una utilidad SQLWRITEEXEC para ejecutar múltiples operaciones de base de datos dependientes dentro de una transacción. Admite operaciones estatales y con estado, que permite flujos de trabajo sofisticados como insertar entidades relacionadas mientras se administra las dependencias sin problemas.
En aplicaciones del mundo real, las operaciones de la base de datos rara vez están aisladas. Considere estos escenarios:
insertar a un usuario y actualizar su inventario atómicamente.
Crear un pedido y procesar su pago, asegurando la consistencia.
Con múltiples pasos involucrados, la gestión de reversiones durante las fallas se vuelve crucial para garantizar la integridad de los datos.
Si está escribiendo una base de datos txn, puede haber varias placas de caldera que podría considerar antes de escribir la lógica central. Si bien esta administración de TXN es administrada por Spring Boot en Java y nunca molestó mucho en ellos mientras escribe código en Java, pero este no es el caso en Golang. Se proporciona un ejemplo simple a continuación
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 }
No podemos esperar repetir el código de reversión/confirmación para cada función. Tenemos dos opciones aquí, ya sea crear una clase que proporcionará una función como un tipo de retorno que, cuando se ejecute en el aplazamiento, cometirá/revertir txn o creará una clase de envoltorio que envuelve todas las funciones TXN juntas y ejecutará de una vez.
fui con la opción posterior y el cambio en el código se puede ver a continuación.
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á mucho más preciso y conciso.
La idea es aislar el TXN a una sola estructura de Go de modo que pueda aceptar múltiples TXN. Por txn me refiero a funciones que harán acción con el txn que creamos para la clase.
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
Estos dos son tipos de funciones que tomarán un TXN para procesar algo. Ahora en la capa de datos implementando una función como esta y pasarla a la clase de ejecutor que se encarga de inyectar los ARG y ejecutar la función.
// 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 }
Aquí es donde almacenamos todos los detalles txn_fn y tendremos commit () método para intentar cometer el 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 }
puede encontrar más ejemplos y pruebas en el repositorio -
https://github.com/mahadev-k/go-utils/tree/main/examples+
avíseme si alguien desea contribuir y construir sobre esto!
¡Gracias por leer tan lejos!
https://in.linkedin.com/in/mahadev-k-934520223ile&&&]
https://x.com/mahadev_k_igh_&&&]
Descargo de responsabilidad: Todos los recursos proporcionados provienen en parte de Internet. Si existe alguna infracción de sus derechos de autor u otros derechos e intereses, explique los motivos detallados y proporcione pruebas de los derechos de autor o derechos e intereses y luego envíelos al correo electrónico: [email protected]. Lo manejaremos por usted lo antes posible.
Copyright© 2022 湘ICP备2022001581号-3