Go database/sql チュートリアル 05 - データ修正とトランザクションの使用

"Modifying Data and Using Transactions" from database/sql tutorial

Go database/sqlチュートリアルのデータ修正とトランザクションの使用の日本語意訳になります。


データ修正とトランザクションの使用

これまでのチュートリアルで、データ変更とトランザクション処理をする方法の準備が整いました。
行の取得とデータ更新に「ステートメント」オブジェクトを使用するプログラミング言語に慣れていれば、その違いは人為的に見えるかもしれませんが、Goではその違いに重要な理由があります。

データ更新のステートメント

INSERT文・UPDATE文・DELETE文、そして行を返却しない他のSQLステートメントでは、Exec()を使います。その際はプリペアドステートメントも一緒に使うことが望ましいです。
次の例では行の挿入と操作に関するメタデータを検査する方法を示しています。

stmt, err := db.Prepare("INSERT INTO users(name) VALUES(?)")
if err != nil {
	log.Fatal(err)
}
res, err := stmt.Exec("Dolly")  // => "INSERT INTO users(name) VALUES('Dolly')"
if err != nil {
	log.Fatal(err)
}
lastId, err := res.LastInsertId()  // 挿入した行のIDを返却
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)

SQL文を実行すると、メタデータ(挿入した行のIDや影響を受けた行数)にアクセス可能なsql.Resultが生成されます。

もしも結果を気にしない場合はどうでしょうか?エラーの発生はチェックするけど、結果は無視する場合はどうでしょうか?
次の2つのステートメントは同じではないのでしょうか?

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

答えはNOです。これらは同じではありません。Query()をこのように使ってはいけません。Query()sql.Rowsを返却しますが、これはクローズされるまでデータベース・コネクションを保ちます。未読データがある可能性があるため(例えばより多くの行)、このコネクションは使用できません。
上記の例では、コネクションはずっと解放されないでしょう。ガベージコレクタは最終的には内部のnet.Connをクローズしますが、それには長い時間がかかるかもしれません。さらにdatabase/sqlパッケージは、あなたがある時点でコネクションを解放することを想定して、プール内のコネクションを再利用できるように追跡し続けます。
このアンチパターンはリソース枯渇(例えばコネクション超過)の典型例となります。

トランザクション処理

Goでは、基本的にトランザクションはデータストアへのコネクションを予約するオブジェクトです。トランザクションでは、これまで説明してきた全ての操作を実行できますが、同じコネクション内で実行されることが保証されています。

db.Begin()を呼び出してトランザクションを開始し、その戻り値のTx変数でCommit()Rollback()メソッドを呼び出してクローズします。内部では、Txはプールからコネクションを取得し、そのトランザクション内のみで使用できるように予約します。
Txのメソッドは、Query()のようにデータベース自体で呼び出すことができるメソッドと1対1で対応しています。

トランザクション内で生成されたプリペアドステートメントは、そのトランザクションにて排他的に結び付けられます。詳細はプリペアドステートメントを参照してください。

Begin()Commit()等のトランザクション関連の関数と、BEGIN, COMMITのようなSQL文を組み合わせて使わないでください。以下のような悪い結果を引き起こします。

  • Txオブジェクトは開いたままになり、プールからコネクションが予約され、解放されなくなります。
  • データベースの実際の状態と、その状態が保存されたGoの変数との同期がされない可能性があります。
  • トランザクションを使った単一のコネクションでクエリを実行していると思うかもしれませんが、実際にはGoがいくつかのコネクションを裏で生成して、いくつかのSQL文はトランザクションの一部ではなくなります。

トランザクション内で処理をしている間は、Db変数を呼び出さないように気をつけてください。db.Begin()で作成したTx変数を使うようにします。Dbはトランザクションではありません。Txだけがトランザクションになります。
もしdb.Exec()のようなものを呼び出すと、他のコネクションのトランザクション範囲外で行われるでしょう。

コネクションの状態を変更するような複数のSQL文を使う必要がある場合、トランザクション自体が不要でも、Txが必要になります。
例えば、

  • 1つのコネクションからのみ表示できる一時テーブルの作成
  • MySQLの@var := somevalue構文のような、変数の設定
  • 文字セットやタイムアウトのような、接続オプションの変更

これらを行う必要がある場合は、アクティビティを単一のコネクションへとバインドする必要があります。そしてそれをGoで実行する唯一の方法はTxを使うことです。


前章: 結果の取得
次章: プリペアドステートメント

 
comments powered by Disqus