go-sql-driver/mysql でプレースホルダ置換をサポートしました
前回の記事で少し触れましたが、 go-sql-driver/mysql にドライバ側でのプレースホルダ置換を実装するプルリクエストを出していました。
それがマージされたので、背景のおさらいと利用方法を紹介しておきます。
背景
Go の database/sql
の概要については前回の記事で解説しました。
そこで説明したとおり、 DB.Prepare()
を使わずに直接 DB.Exec()
や DB.Query()
を使った場合、
ドライバ側でのプレースホルダ置換に対応していないドライバでは prepare, exec, close
で3回のラウンドトリップが発生することになり、パフォーマンスが悪くなります。
基本的には DB.Prepare()
を使えばいいのですが、前回の記事で修正したスケーラビリティの問題は Go 1.5 になるまで直りませんし、
IN
句があるSQL文などで事前に Prepare するのが難しいケースも有ります。
また、O/Rマッパーやクエリビルダーを使う場合、そのライブラリが Prepared Statement のキャッシュに対応していないと、
アプリケーションプログラマが制御できない部分で DB.Prepare()
を使わないで DB.Query()
が使われるケースが出てきます。
ちょうど prepared statement をクエリの都度発行するデメリットが 幾つかの
場所で 話題になったこともあり、 go-sql-driver/mysql
にオプションを付与する RFC (Request For Comment)
だったプルリクエストがマージに向けて動き始めました。(マージされるまでに行ったチューニングについてはまた次回紹介しようと思います。)
go-sql-driver/mysql のプレースホルダ置換について
go-sql-drvier/mysql
は go get -u github.com/go-sql-driver/mysql
で取得します。
まだこの機能はマージされたばかりなので、すでに利用されている方は再度このコマンドを実行して更新してください。
sql.Open()
の第二引数に渡す dsl で interpolateParams=true
というオプションをURLのクエリパラメタの形で渡すことにより、プレースホルダ置換が有効になります。
ただし、文字列をエスケープする際にバイト単位で処理しているので、 collation
オプションで cp932
や big5
など危険なエンコーディングを指定した場合はエラーにしています。
(ちなみに、 go-sql-driver/mysql でコネクションのエンコーディングを指定する charset
オプションは非推奨なので、 collation
オプションを利用してください)
collationは指定しない場合デフォルトで utf8_general_ci
ですが、次の例では utf8mb4_bin
に指定しています。
package main import ( "database/sql" "log" _ "github.com/go-sql-driver/mysql" ) func main() { // localhost の 3306 番ポートで動いている MySQL に、ユーザー名=root, パスワード=password, データベース=test で接続する。 // interpolateParams=true オプションを利用する。 db, err := sql.Open("mysql", "root:password@tcp(localhost:3306)/test?interpolateParams=true&collation=utf8mb4_bin") // プレースホルダ置換を行わない場合はこちら //db, err := sql.Open("mysql", "root:password@tcp(localhost:3306)/test?collation=utf8mb4_bin") if err != nil { log.Fatal(err) } if _, err := db.Exec("SELECT SLEEP(?)", 42); err != nil { log.Fatal(err) } }
このプログラムを実行してる時、別のセッションから SHOW FULL PROCESSLIST
を実行すると、オプションの効果が確認できます。
interpolateParams=true
を付けない場合は、次のように Command
が Execute
になり、 Info
ではプレースホルダがそのまま表示されます。(とてもタイミングが良ければ、 Execute
コマンドじゃなくて Prepare
コマンドが見えるかもしれません)
mysql> show full processlist; +------+------+-----------------+------+---------+------+------------+-----------------------+ | Id | User | Host | db | Command | Time | State | Info | +------+------+-----------------+------+---------+------+------------+-----------------------+ | 7508 | root | localhost | NULL | Query | 0 | init | show full processlist | | 7511 | root | localhost:55283 | test | Execute | 2 | User sleep | SELECT SLEEP(?) | +------+------+-----------------+------+---------+------+------------+-----------------------+ 2 rows in set (0.00 sec)
interpolateParams=true
を付けた場合は、次のように Command
が Query
になり、 Info
でプレースホルダの部分に値が挿入されていることを確認できます。
mysql> show full processlist; +----+------+-----------------+------+---------+------+------------+-----------------------+ | Id | User | Host | db | Command | Time | State | Info | +----+------+-----------------+------+---------+------+------------+-----------------------+ | 2 | root | localhost | NULL | Query | 0 | init | show full processlist | | 3 | root | localhost:49283 | test | Query | 1 | User sleep | SELECT SLEEP(42) | +----+------+-----------------+------+---------+------+------------+-----------------------+ 2 rows in set (0.00 sec)
文字列のエスケープでは、 sql_mode
に NO_BACKSLASH_ESCAPES
を設定した場合にも対応しています。
次の結果は db.Exec("SELECT ?, SLEEP(?)", "Hel'lo\nWorld", 42)
した時のものです。
mysql> SELECT @@GLOBAL.sql_mode; +-----------------------------------------------------------------+ | @@GLOBAL.sql_mode | +-----------------------------------------------------------------+ | NO_BACKSLASH_ESCAPES,STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION | +-----------------------------------------------------------------+ 1 row in set (0.00 sec) mysql> show full processlist; +------+------+-----------------+------+---------+------+------------+-----------------------------------+ | Id | User | Host | db | Command | Time | State | Info | +------+------+-----------------+------+---------+------+------------+-----------------------------------+ | 7515 | root | localhost | NULL | Query | 0 | init | show full processlist | | 7517 | root | localhost:55400 | test | Query | 2 | User sleep | SELECT 'Hel''lo World', SLEEP(42) | +------+------+-----------------+------+---------+------+------------+-----------------------------------+ 2 rows in set (0.00 sec)
@methane