トランザクションの実行

トランザクションを実行するには、トランザクションを表すsql.Txを使用します。sql.Txには、トランザクション固有のセマンティクスを表すCommitメソッドとRollbackメソッドに加え、一般的なデータベース操作を実行するために使用するすべてのメソッドが含まれています。sql.Txを取得するには、DB.BeginまたはDB.BeginTxを呼び出します。

データベーストランザクションは、より大きな目標の一部として複数の操作をグループ化します。すべての操作が成功するか、またはどれも成功しないかのどちらかであり、どちらの場合もデータの整合性が保たれます。通常、トランザクションのワークフローには以下が含まれます。

  1. トランザクションの開始。
  2. 一連のデータベース操作の実行。
  3. エラーが発生しない場合、データベースの変更を行うためにトランザクションをコミット。
  4. エラーが発生した場合、データベースが変更されないようにトランザクションをロールバック。

sqlパッケージは、トランザクションを開始および終了するためのメソッド、およびその間のデータベース操作を実行するためのメソッドを提供します。これらのメソッドは、上記のワークフローの4つのステップに対応しています。

  • トランザクションを開始します。

    DB.BeginまたはDB.BeginTxは、新しいデータベーストランザクションを開始し、それを表すsql.Txを返します。

  • データベース操作を実行します。

    sql.Txを使用すると、単一の接続を使用する一連の操作でデータベースをクエリまたは更新できます。これをサポートするために、Txは次のメソッドを公開しています。

  • 以下のいずれか1つでトランザクションを終了します。

    • Tx.Commitを使用してトランザクションをコミットします。

      Commitが成功した場合(nilエラーを返した場合)、すべてのクエリ結果が有効として確認され、実行されたすべての更新が単一のアトミックな変更としてデータベースに適用されます。Commitが失敗した場合、Tx上のQueryおよびExecからのすべての結果は無効として破棄されるべきです。

    • Tx.Rollbackを使用してトランザクションをロールバックします。

      Tx.Rollbackが失敗したとしても、トランザクションは無効になり、データベースにコミットされません。

ベストプラクティス

以下に示すベストプラクティスに従い、トランザクションが時々要求する複雑なセマンティクスと接続管理をより適切に処理してください。

  • トランザクションを管理するには、このセクションで説明されているAPIを使用します。BEGINCOMMITなどのトランザクション関連のSQLステートメントを直接使用しないでください。これを行うと、特に並行プログラムにおいて、データベースが予測不可能な状態になる可能性があります。
  • トランザクションを使用している場合は、非トランザクションのsql.DBメソッドを直接呼び出さないように注意してください。これらのメソッドはトランザクションの外部で実行され、コードにデータベースの状態の一貫性のないビューを与えたり、デッドロックを引き起こしたりする可能性があります。

次の例のコードは、トランザクションを使用してアルバムの新しい顧客注文を作成します。このコードは、その過程で次の処理を行います。

  1. トランザクションを開始します。
  2. トランザクションのロールバックを遅延させます。トランザクションが成功した場合、関数が終了する前にコミットされ、遅延されたロールバック呼び出しはノーオペレーションになります。トランザクションが失敗した場合はコミットされず、関数が終了するときにロールバックが呼び出されます。
  3. 顧客が注文しているアルバムの在庫が十分であることを確認します。
  4. 十分な在庫がある場合、在庫数を更新し、注文されたアルバムの数だけ減らします。
  5. 新しい注文を作成し、クライアント用に新しく生成された注文IDを取得します。
  6. トランザクションをコミットし、IDを返します。

この例では、context.Context引数を取るTxメソッドを使用しています。これにより、実行時間が長すぎたり、クライアント接続が閉じたりした場合に、データベース操作を含む関数の実行をキャンセルすることができます。詳細については、進行中の操作のキャンセルを参照してください。

// CreateOrder creates an order for an album and returns the new order ID.
func CreateOrder(ctx context.Context, albumID, quantity, custID int) (orderID int64, err error) {

    // Create a helper function for preparing failure results.
    fail := func(err error) (int64, error) {
        return 0, fmt.Errorf("CreateOrder: %v", err)
    }

    // Get a Tx for making transaction requests.
    tx, err := db.BeginTx(ctx, nil)
    if err != nil {
        return fail(err)
    }
    // Defer a rollback in case anything fails.
    defer tx.Rollback()

    // Confirm that album inventory is enough for the order.
    var enough bool
    if err = tx.QueryRowContext(ctx, "SELECT (quantity >= ?) from album where id = ?",
        quantity, albumID).Scan(&enough); err != nil {
        if err == sql.ErrNoRows {
            return fail(fmt.Errorf("no such album"))
        }
        return fail(err)
    }
    if !enough {
        return fail(fmt.Errorf("not enough inventory"))
    }

    // Update the album inventory to remove the quantity in the order.
    _, err = tx.ExecContext(ctx, "UPDATE album SET quantity = quantity - ? WHERE id = ?",
        quantity, albumID)
    if err != nil {
        return fail(err)
    }

    // Create a new row in the album_order table.
    result, err := tx.ExecContext(ctx, "INSERT INTO album_order (album_id, cust_id, quantity, date) VALUES (?, ?, ?, ?)",
        albumID, custID, quantity, time.Now())
    if err != nil {
        return fail(err)
    }
    // Get the ID of the order item just created.
    orderID, err = result.LastInsertId()
    if err != nil {
        return fail(err)
    }

    // Commit the transaction.
    if err = tx.Commit(); err != nil {
        return fail(err)
    }

    // Return the order ID.
    return orderID, nil
}