Go database/sql チュートリアル 06 - プリペアドステートメントの使用

"Using Prepared Statements" from database/sql tutorial

Go database/sqlチュートリアルのプリペアドステートメントの使用の日本語意訳になります。


プリペアドステートメントの使用

プリペアドステートメントは、Goのよくある利点を全て備えています。セキュリティ、効率性、利便性です。しかしそれは慣れ親しんだ方法とは少し異なったやり方で実装されていて、database/sql内部のいくつか箇所とやりとりを行う方法は特にそうです。

プリペアドステートメントとコネクション

データベースレベルでは、プリペアドステートメントは単一のコネクションに紐付けられています。典型的な流れとしては、クライアントはプレースホルダ付きのSQL文を、準備(プリペア)用にサーバーへ送り、サーバーはステートメントIDを返却します。そして、クライアントがそのIDとパラメータを送ることで、SQL文を実行します。

しかし、Goでは、コネクションはdatabase/sqlパッケージの使用者には直接公開されていません。コネクションでステートメントを準備するのではなく、DBTxで準備します。database/sqlには例えば自動リトライのような便利な動作があります。これらの理由により、プリペアドステートメントとコネクションの実際の連携は、ドライバレベルで存在しており、あなたのコード上からは隠されてます。

以下のような動作をします。

  • SQL文をプリペアすると、プール内のコネクション上でプリペアされます。
  • Stmtオブジェクトはどのコネクションが使われたかを記憶します。
  • そのStmtを実行すると、そのコネクションを使用しようとします。もしもクローズされていたり他の処理をしていて利用できない場合は、プールから他のコネクションを取得し、データベースの他のコネクションで再度SQL文をプリペアします。

元のコネクションがビジー状態の時、SQL文は必要に応じて再度プリペアされるため、データベースの高並列利用が可能です。高並列利用は多くのコネクションをビジー状態のままにして、大量のプリペアドステートメントを生成します。
これは明らかにステートメントのリークが発生する可能性があり、あなたが考えるよりプリペアと再プリペアを頻繁に行い、サーバー側でステートメント数の上限に達することさえあるでしょう。

プリペアドステートメントを避ける

Goはあなたのために見えない所でプリペアドステートメントを作成します。例えば簡単なdb.Query(sql, param1, param2)ではSQL文をプリペアし、パラメータ付きで実行し、最後にステートメントをクローズしています。

しかしながら、プリペアドステートメントはあなたが望むものではない場合もあります。これにはいくつかの理由があるでしょう。

  • データベースがプリペアドステートメントをサポートしていない場合。例えばMySQLドライバを使って、MemSQLやSphinxへ接続することができます。それらがMySQLのWireプロトコルをサポートしているためです。しかしプリペアドステートメントを含んでいる「バイナリ」プロトコルはサポートしていません。そのため紛らわしい方法で失敗する可能性があります。
  • ステートメントが頻繁に再利用されず、セキュリティに関する問題は他の方法で処理されるため、パフォーマンスのオーバーヘッドが望ましくない場合。この例はVividCortexのブログで見ることができます。

もしプリペアドステートメントを使いたくない場合は、fmt.Sprint()等を使ってSQLを組み立て、そのSQLをdb.Query()db.QueryRow()へ唯一の引数として渡す必要があります。これはGo1.1で追加されたExecuterQueryerインターフェース(ドキュメントはこちら)経由で、プレーンテキストクエリの実行をドライバがサポートしている必要があります。

トランザクション内でのプリペアドステートメント

Txで作成されたプリペアドステートメントはそのTx専用に紐付けられているため、再プリペアに関する前述の注意事項は当てはまりません。Txオブジェクトの操作をする時、アクションはそのTxとそれに紐付けられた単一のコネクションに直接マッピングされます。

これはTx内で作成されたプリペアドステートメントはそのTxと切り離して使うことができないということも意味しています。同様に、DB上で作成されたプリペアドステートメントは、違うコネクションに紐付けられてしまうため、トランザクション内で使うことはできません

トランザクション外部でプリペアされたプリペアドステートメントをTx内で使うには、Tx.Stmt()を使うことができます。これはトランザクション外でプリペアされたステートメントから、新しいトランザクション固有のステートメントを生成します。
これは既存のプリペアドステートメントを取り、コネクションをそのトランザクションへと設定し、実行の度に毎回全てのステートメントを再プリペアします。
この振る舞いと実装は望ましくなく、database/sqlのソースコードには改善のためのTODOさえ存在しています。そのため、わたしたちはこれを使わないことをオススメします。

トランザクション内でプリペアドステートメントを使う場合は注意してください。
次の例を考えてみましょう。

tx, err := db.Begin()
if err != nil {
	log.Fatal(err)
}
defer tx.Rollback()
stmt, err := tx.Prepare("INSERT INTO foo VALUES (?)")
if err != nil {
	log.Fatal(err)
}
defer stmt.Close() // これは危険!
for i := 0; i < 10; i++ {
	_, err = stmt.Exec(i)
	if err != nil {
		log.Fatal(err)
	}
}
err = tx.Commit()
if err != nil {
	log.Fatal(err)
}
// stmt.Close() はここで実行されます!

Go1.4より前では、*sql.Txをクローズすると関連付けられたコネクションが解放されプールへ返却されていました。しかしプリペアドステートメントのCloseのdeferによる呼び出しは、それがTxのクローズが発生した後で実行されてしまいます。これは実際のコネクションへの同時アクセスを引き起こす可能性があり、矛盾した接続状態にしてしまいます。Go1.4かそれ以前を使う場合は、トランザクションがコミットやロールバックされる前に、ステートメントが常にクローズされている状態であることを確認する必要があります。
この問題はGo1.4のCR 131650043で修正されています。

パラメータ プレースホルダ構文

プリペアドステートメントでのプレースホルダパラメータの構文はデータベースにより異なります。
例として、MySQL・PostgreSQL・Oracleを比較してみましょう。

MySQL               PostgreSQL            Oracle
=====               ==========            ======
WHERE col = ?       WHERE col = $1        WHERE col = :col
VALUES(?, ?, ?)     VALUES($1, $2, $3)    VALUES(:val1, :val2, :val3)

前章: 更新とトランザクション
次章: エラーハンドリング

 
comments powered by Disqus