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/sqlgithub.com/lib/pq をimport
  • sql.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 パッケージへのドライバの登録を行なっています。

具体的に何をやっているのかについては、pqinit() 関数 を見てみましょう。

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.DBconnector プロパティにimportしたドライバから生成した driver.Connector をセットしていることです。この connector はのちにデータベースに接続するときに使われます。

まとめ

とりあえず sql.Open するところまで動作を追ってみましたが、database/sql とドライバの関係がうまく設計されていて面白かったです。次は db.Query でレコードを取得して、そのレコードの内容を取り出すところを見ていきます。ちなみに、goの勉強は標準パッケージを読むのが良いと何回か聞いたことがありますが、今回実際に読んでみてたしかにいろいろ勉強になったので、引き続きいろいろ読んでいこうかなという思っています。