"Se um trabalhador quiser fazer bem o seu trabalho, ele deve primeiro afiar suas ferramentas." - Confúcio, "Os Analectos de Confúcio. Lu Linggong"
Primeira página > Programação > Construindo a execução robusta da transação SQL em Go com uma estrutura genérica

Construindo a execução robusta da transação SQL em Go com uma estrutura genérica

Postado em 2025-03-23
Navegar:837

Building Robust SQL Transaction Execution in Go with a Generic Framework

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.

Por que precisamos de uma estrutura para transações SQL?

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.

Trabalhando com o Go em TXN Management.

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.

Como a lógica principal é implementada

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/ !

deixe -me saber se alguém deseja contribuir e construir sobre isso !!

Obrigado por ler até agora !!

https://in.linkedin.com/in/mahadev-k-934520223

https://x.com/mahadev_k_


Declaração de lançamento Este artigo é reproduzido em: https://dev.to/mahadev_k/building-robust-sql-ransaction-execution-in-go-with-a-generic-framework-4j0f?1 Se houver alguma violação, entre em contato com [email protected] para deletá-lo.
Tutorial mais recente Mais>

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