【问题标题】:Raw sql transactions with golang prepared statements带有 golang 准备语句的原始 sql 事务
【发布时间】:2017-02-25 20:37:27
【问题描述】:

我很难找到一些执行以下三项操作的示例:

1) 在 golang 中允许原始 sql 事务。

2) 使用准备好的语句。

3) 查询失败时回滚。

我想做这样的事情,但有准备好的陈述。

    stmt, stmt_err := db.Prepare(`
            BEGIN TRANSACTION;

            -- Insert record into first table.

            INSERT INTO table_1 (
                    thing_1,
                    whatever)
            VALUES($1,$2);

            -- Inert record into second table.

            INSERT INTO table_2 (
                    thing_2,
                    whatever)
            VALUES($3,$4);

            END TRANSACTION;
            `)
    if stmt_err != nil {
            return stmt_err
    }   
    res, res_err := stmt.Exec(
            thing_1,
            whatever,
            thing_2,
            whatever)

当我运行它时,我得到这个错误: pq: cannot insert multiple commands into a prepared statement

什么给了?在 golang 中是否可以进行符合 ACID 的事务?我找不到例子。

编辑 没有例子here

【问题讨论】:

    标签: postgresql go transactions acid


    【解决方案1】:

    是的,Go 有一个很好的 sql transactions 实现。我们以db.Begin 开始事务,如果一切顺利,我们可以以tx.Commit 结束它,或者以tx.Rollback 结束它以防出错。

    类型 Tx 结构 { }

    Tx 是一个正在进行的数据库事务。

    事务必须以调用 Commit 或 Rollback 结束。

    在调用 Commit 或 Rollback 后,事务上的所有操作都会失败并显示 ErrTxDone。

    通过调用事务的 Prepare 或 Stmt 方法为事务准备的语句通过调用 Commit 或 Rollback 关闭。

    还请注意,我们使用事务变量 tx.Prepare(...) 准备查询

    您的函数可能如下所示:

    func doubleInsert(db *sql.DB) error {
    
        tx, err := db.Begin()
        if err != nil {
            return err
        }
    
        {
            stmt, err := tx.Prepare(`INSERT INTO table_1 (thing_1, whatever)
                         VALUES($1,$2);`)
            if err != nil {
                tx.Rollback()
                return err
            }
            defer stmt.Close()
    
            if _, err := stmt.Exec(thing_1, whatever); err != nil {
                tx.Rollback() // return an error too, we may want to wrap them
                return err
            }
        }
    
        {
            stmt, err := tx.Prepare(`INSERT INTO table_2 (thing_2, whatever)
                         VALUES($1, $2);`)
            if err != nil {
                tx.Rollback()
                return err
            }
            defer stmt.Close()
    
            if _, err := stmt.Exec(thing_2, whatever); err != nil {
                tx.Rollback() // return an error too, we may want to wrap them
                return err
            }
        }
    
        return tx.Commit()
    }
    

    我有一个完整的例子here

    【讨论】:

    • 非常有用的帖子,谢谢。但是我不明白,A transaction must end with a call to Commit or Rollback.,当您返回由tx.Prepare(some sql..) 引起的错误时,您不会执行提交或回滚。这是为什么?在那种情况下,有问题的交易是否正确关闭?即使底层数据库没有做任何更改,我们是否仍然需要正确关闭事务?
    • 此代码对于 Go1.4 或更早版本不正确,tx.Commit() 会将与其关联的连接释放回池中,这将在 stmt.Close() 之前发生,这可能导致并发访问底层连接,导致连接状态不一致。如此处所述:go-database-sql.org/prepared.html
    • @YandryPozo 这对我来说听起来不对。如上所述,根据golang.org/pkg/database/sql/#Tx,事务必须以 Rollback 或 Commit 结束。根据同一个文档,Tx 是一个交易,所以只要你有一个 Tx,你就有一个交易。
    • @MarceloCantos 你是对的,如果我们有一个已创建的交易,我们无论如何都必须完成它。我刚刚编辑了我的答案,感谢您的关注!
    • @BARJ 你说得对,我编辑了我的答案,谢谢你的评论!
    【解决方案2】:

    我想出了一个可能的解决方案,可以在没有任何重大缺陷的情况下回滚任何失败。不过,我对 Golang 还是很陌生,我可能是错的。

    func CloseTransaction(tx *sql.Tx, commit *bool) {
      if *commit {
        log.Println("Commit sql transaction")
        if err := tx.Commit(); err != nil {
          log.Panic(err)
        }
      } else {
        log.Println("Rollback sql transcation")
        if err := tx.Rollback(); err != nil {
          log.Panic(err)
        }
      }
    }
    
    func MultipleSqlQuriesWithTx(db *sql.DB, .. /* some parameter(s) */)  (.. .. /* some named return parameter(s) */, err error) {
      tx, err := db.Begin()
      if err != nil {
        return
      }
      commitTx := false
      defer CloseTransaction(tx, &commitTx)
    
      // First sql query
      stmt, err := tx.Prepare(..) // some raw sql
      if err != nil {
        return
      }
      defer stmt.Close()
    
      res, err := stmt.Exec(..) // some var args
      if err != nil {
        return
      }
    
      // Second sql query
      stmt, err := tx.Prepare(..) // some raw sql
      if err != nil {
        return
      }
      defer stmt.Close()
    
      res, err := stmt.Exec(..) // some var args
      if err != nil {
        return
      }
    
      /*
        more tx sql statements and queries here
      */
    
      // success, commit and return result
      commitTx = true
      return
    }
    

    【讨论】: