database/sql パッケージでクエリを投げるまでに起こること(1)
よくgoのO/Rマッパーとして gorm を使っているのですが、gormの挙動ではまったりすると内部で使っている標準の database/sql
パッケージの理解が必要になることがあります。これまで直接 database/sql
を使ったことがなかったので、そのクエリを投げるまでの内部動作について調べてみます。今日は、とりあえず sql.Open
するところまで動作を追ったのでそこまで。
サンプルコード
まずは、database/sql
の基本的な使い方として、クエリを投げて複数のレコードを取得する場合にどのようなコードになるかを見てみます。例として、足が6本以上ある動物の一覧が知りたくなった場合のコードを挙げてみます。
package main import ( "database/sql" "fmt" _ "github.com/lib/pq" ) func main() { db, err := sql.Open("postgres", "host=127.0.0.1 port=32768 user=postgres dbname=playground sslmode=disable") if err != nil { panic(err) } defer db.Close() rows, err := db.Query(`SELECT animal, legs FROM animals WHERE legs >= $1`, 6) if err != nil { panic(err) } defer rows.Close() var ( animal string legs int ) for rows.Next() { if err := rows.Scan(&animal, &legs); err != nil { panic(err) } fmt.Printf("%s has %d legs\n", animal, legs) } if rows.Err() != nil { panic(err) } }
postgreSQL を使うことを想定して、ドライバとして github.com/lib/pq
を使っていますが、他のRDBでもimportするドライバを変えれば同じように動くと思います。また、サンプルコードの簡素化のためエラー処理では単に panic
しています。
わざわざ解説するほどのことはないですが、正常に処理が進むと以下のような流れになります。
database/sql
とgithub.com/lib/pq
をimportsql.Open
で*sql.DB
オブジェクトを取得db.Query
でクエリを投げて結果を*sql.Rows
として取得rows.Scan
で結果を変数にマッピングrows.Err
でエラーチェックrows.Close
db.Close
順に、内部でどのような処理がされているのか見ていきましょう。
import
まずはimportからです。
import ( "database/sql" _ "github.com/lib/pq" )
database/sql
パッケージと、ドライバである github.com/lib/pq
パッケージをimportしています。ここで、ドライバの方のimport分にはアンダースコアが使われていますね。このようにimportすることで、pq
パッケージでexportされているものが見えなくなります。パッケージでexportしているオブジェクトが使えなかったらimportする意味がなさそうに見えますが、そんなことはありません。pq
パッケージはimportされる際の副作用として、sql
パッケージへのドライバの登録を行なっています。
具体的に何をやっているのかについては、pq
の init()
関数 を見てみましょう。
func init() { sql.Register("postgres", &Driver{}) }
sql
パッケージの Register
関数に自パッケージの Driver
インスタンスのポインタを渡しています。次に、Register
関数 の方を見てみましょう。
func Register(name string, driver driver.Driver) { driversMu.Lock() defer driversMu.Unlock() if driver == nil { panic("sql: Register driver is nil") } if _, dup := drivers[name]; dup { panic("sql: Register called twice for driver " + name) } drivers[name] = driver }
こちらでは、ドライバを保持するグローバル変数である drivers
に渡されたドライバを登録していることがわかります。この渡されたドライバは、接続の確立やクエリの実行など、今後のいろいろな処理で使われることになります。つまり、ドライバをアンダースコアにエイリアスしてimportし、ドライバから我々が書くコードではなく sql
パッケージに影響を与えることで、必ず sql
パッケージを介してドライバが使われるようになります。これは面白いですね。まあ世の中には他にもっと面白いことがあると思いますが...。
sql.Open
先程登録されたドライバを使って、sql.Open
を行い、データベースの操作に使う *sql.DB
インスタンスを取得します。注意点として、ソースコードのコメント にも書かれている通り、sql.Open
ではデータベースへ接続する下準備をするだけで、接続の確立は行われません。少し直感に反していると思いますが、接続の確立はクエリを投げる時など実際に接続が必要になった時に初めて行われます。コメントの同じ箇所に書かれている通り、sql.Open
に渡した引数で正しく接続ができるのかを確認するには、sql.Ping
を呼ぶ必要があります。ちなみに、gormでは、gorm.Open
の中で sql.Open
した後にちゃんとsql.Ping
が呼ばれていました。
さて、接続する下準備とは具体的にはなんなのかという話ですが、主要な処理は *sql.DB
の connector
プロパティにimportしたドライバから生成した driver.Connector
をセットしていることです。この connector
はのちにデータベースに接続するときに使われます。
まとめ
とりあえず sql.Open
するところまで動作を追ってみましたが、database/sql
とドライバの関係がうまく設計されていて面白かったです。次は db.Query
でレコードを取得して、そのレコードの内容を取り出すところを見ていきます。ちなみに、goの勉強は標準パッケージを読むのが良いと何回か聞いたことがありますが、今回実際に読んでみてたしかにいろいろ勉強になったので、引き続きいろいろ読んでいこうかなという思っています。