Fork me on GitHub

トランザクションを使ったデータの変更


これで、トランザクションを用いてデータを操作する準備が整いました。行のフェッチと同様にデータの更新に「ステートメント」オブジェクトを使う言語に慣れている人にとっては、フェッチをトランザクション操作と区別しているのはなにか理由があるのではないかと思うかもしれません。Goでは区別する重要な理由があります。

データを変更するステートメント

INSERT, UPDATE, DELETE やその他の行を返さないステートメントを実行するためには、できる限りプリペアードステートメントを用いて Exec() を呼び出します。以下の例は、行を挿入し、操作に関するメタデータを検査する例です。

stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
    log.Fatal(err)
}
res, err := stmt.Exec("Dolly")
if err != nil {
    log.Fatal(err)
}
lastId, err := res.LastInsertId()
if err != nil {
    log.Fatal(err)
}
rowCnt, err := res.RowsAffected()
if err != nil {
    log.Fatal(err)
}
log.Printf("ID = %d, affected = %d\n", lastId, rowCnt)

ステートメントを実行すると、ステートメントのメタデータ(例えば最後似挿入したIDや影響があった行数など)へアクセスできる sql.Result オブジェクトが生成されます。

結果を気にする必要がない場合はどうすればよいでしょうか。ステートメントを実行してエラーをチェックしたいだけで、結果を無視したい場合はどうでしょうか。次の2つのステートメントは同じように振る舞うでしょうか?

_, err := db.Exec("DELETE FROM users")  // OK
_, err := db.Query("DELETE FROM users") // BAD

答えは No です。上記2つは同じように 振る舞わない ため、Query() を上記のように使用するべきではありません。Query()sql.Rows を返します。これは sql.Rows が閉じられるまでデータベースへのコネクションを保持します。未読のデータが存在する可能性があるため(データの行数が増えるなど)、コネクションを使用することができません。上記の例ではコネクションが開放されることはありません。最終的にガベージコレクタが基点になっている net.Conn を閉じることになりますが、これには時間がかかる場合があります。さらに sql/database パッケージはプール内のコネクションを追跡し続けます。ある時点でコネクションが開放され、コネクションを再利用したいためです。これは「コネクションが多すぎます」などといったリソース不足を引き起こすアンチパターンの一つです。

トランザクションの動作

Goのトランザクションは基本的にデータストアへのコネクションを保持するオブジェクトです。これにより、これまで見てきたすべての操作をすることができます。同じコネクションで実施されることが保証されます。

db.Begin() を呼び出してトランザクションを開始することができます。トランザクションを閉じるには db.Begin() から返される Tx の変数で Commit()Rollback() メソッドを使用します。内部では Tx はプールからコネクションを取得し、トランザクション専用で使用するために保持します。Tx のメソッドは Query() などのデータベースで呼び出すことができるメソッドと1対1で対応します。

トランザクション内で生成されるプリペアードステートメントは、そのトランザクションのみに紐付けられます。詳しくは prepared statements を参照ください。

コード内で BEGINCOMMIT SQLのステートメントと、 Begin()Commit() というトランザクション関連する関数を混ぜるのはやめてください。悪い結果が起こる可能性があります。

  • Tx オブジェクトは開いたままになり、プールからコネクションを確保し、プールに戻しません。

  • データベースの状態がトランザクションを示すGoの変数の状態と不整合になる可能性があります。

  • 実際にはGoがいくつかのユーザに見えないコネクションを生成していて、いくつかのステートメントはトランザクションの一部でない場合、トランザクション内の単一のコネクションでクエリを実行していると考えることができます。

トランザクション内で作業しているときは、 db 変数を呼び出さないように注意する必要があります。 db.Begin() で生成した Tx 変数を用いてメソッドを呼び出します。 Tx のみがトランザクションであって db ではありません。 db.Exec() やそれに似た呼び出しを行うと、別のコネクションでトランザクション外のスコープとして呼び出されます。

トランザクションそれ自体は必要がない場合でも、コネクションの状態を変更する複数のステートメントを動作する必要がある場合は、Tx を使う必要があります。例えば以下のようなものです。

  • 1つのコネクションから参照可能な一時的なテーブルを作成する場合

  • MySQL の SET @var := somevalue 構文といった、変数を設定する場合

  • タイムアウト設定などの、文字コードの設定や、コネクションのオプションを変更する場合

上記のいずれかを行う場合は、操作を単一のコネクション上で実施する必要があります。Goでこれらを行う唯一の方法は Tx を使用することです。