Fork me on GitHub

プリペアードステートメント


Goのプリペアードステートメントは、よく知られた利点(セキュリティ、効率性、利便性)はすべてあります。ただし、それらの実装方法は、特に database/sql の内部のやり取りに関して、よく知られているものとは少し異なるかもしれません。

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

データベースのレイヤーでは、プリペアードステートメントは単一のデータベース接続に紐付けられます。典型的な流れは、クライアントがプレースホルダ付きのSQLステートメントをサーバーに準備として送信します。サーバーからはステートメントIDがレスポンスとして返ってきて、クライアントはレスポンスIDとパラメータを送信することでステートメントを実行します。

しかしGoでは database/sql パッケージのユーザーには直接コネクションが公開されることはありません。接続に関するステートメントを準備する必要はありません。DBTx を用いて準備します。そして database/sql は自動リトライなどのいくつかの便利な機能があります。これらの理由によってドライバーのレイヤで存在するプリペアードステートメントとコネクションの紐付けは、アプリケーションの実装には表れません。

仕組みは次のとおりです。

  1. ステートメントを準備すると、プールからコネクションが準備されます。

  2. Stmt オブジェクトはどのコネクションが使われているか把握します。

  3. Stmt を実行すると、コネクションを使うように試行します。もしコネクションが閉じているか、別の何かの操作でビジーになっていて、コネクションが使用できない場合、プールから別のコネクションを取得し 別のコネクションでステートメントを再準備します

元のコネクションがビジーの場合、必要に応じてステートメントは再準備されます。多数の接続がビジー状態のままになるような高い並列度で使用することができ、たくさんのプリペアードステートメントを作成することができます。これにより、ステートメントのリークをもたらす可能性があります。予想よりも頻繁に再準備され、サーバー側ではステートメントの数が制限されることさえあります。

プリペアードステートメントを使わない場合

Goは内部でプリペアードステートメントを作成します。簡単な例として db.Query(sql, param1, param2) はSQLを準備し、パラメータを受け取って実行され、ステートメントが閉じられることで機能します。

いくつかの場面で、プリペアードステートメントが必要でない場合もあります。以下の理由によるものです。

  1. データベースがプリペアードステートメントをサポートしていない場合。例えばMySQLドライバーを使ってMemSQLやSphinxに接続することができますが、これはMySQLのWireプロトコルをサポートしているためです。しかしプリペアードステートメントを含む「バイナリ」のプロトコルはサポートしていないため、わかりにくい理由で失敗することがあります。

  2. ステートメントは再利用する価値がなく、セキュリティの問題は別の方法で扱われる場合です。プリペアードステートメントはパフォーマンスの観点で望ましくないためです。この例は VividCortex blog で見ることができます。

プリペアードステートメントを使用したくない場合は、fmt.Sprint() などを用いてSQLを組み立てる必要があり、db.Query()db.QueryRow() に唯一の引数として渡す必要があります。またドライバーは素のテキストクエリの実行をサポートする必要があります。これはGo1.1で追加された ExecerQueryer インターフェースを実装します。詳細は documented here を参照ください。

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

Tx 内で作成されたプリペアードステートメントは Tx のみに紐付いているため、再準備に関する前のセクションで説明した注意事項は影響ありません。Tx オブジェクトを操作する場合、その操作はバックグラウンドの唯一のコネクションにのみ直接マッピングされます。

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

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() // danger!
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() runs here!

Go1.4以前は *sql.Tx をCloseするとそれに紐付いているコネクションを開放し、プールに戻します。しかしプリペアードステートメントでCloseを遅延呼び出しすることは、トランザクションが発生した に実行されました。これはコネクションへの同時アクセスにつながり、接続状態に一貫性がありません。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)