Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Goのsqlパッケージを理解する

サーバーサイドエンジニアの森谷です。

弊社ではDBの一部にSQLiteを使っており、SQLiteからのSELECT結果を構造体にパースする部分の処理について調べごとをしていたのですが、その中で「SELECT結果をモックできたら楽に手元で色々確認できないかな」という疑問が湧いてきました。
ということで(?)sqlとsqlxパッケージを理解し、モックができるのかどうか見ていこうと思います!

なお、sqlパッケージのみで既に巨大な記事になってしまったため、今回はsqlパッケージのみを対象とし、次回の記事でsqlxパッケージに触れようと思います。

本記事の内容について

触れること

sqlパッケージにおいて各種ドライバーごとのクエリがどのように実行され解釈されるかを見ていきます。

触れないこと

コネクションの管理やゴルーチン間のロックの管理にまで手を出すと記事のボリュームが5倍くらいになりそうなので、これらについては残念ながら割愛させていただきます。

まずはDriverの登録処理を追ってみる

まず初めにmysqlドライバーを参考にコードを追ってみます。

各ドライバーを使用する際にはimport文にてpackageを指定します。
ただし、このpackageの関数などを我々が直接使用するわけではないため _ でimportします。

import _ "github.com/go-sql-driver/mysql"

packageが初めてimportされた時、そのpackage内のinit関数が実行されます。
packageを直接使用しないのにimportする理由は、このinitを実行したい(逆に言うと、このinitさえ実行できれば全てが完結する)からです。

func init() {
    sql.Register("mysql", &MySQLDriver{})
}

https://github.com/go-sql-driver/mysql/blob/bcc459a906419e2890a50fc2c99ea6dd927a88f2/driver.go#L83-L85

sql.Register は第2引数に driver.Driver を取り、sqlパッケージ直下の drivers map[string]driver.Driver 変数に追加していきます。
https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/database/sql/sql.go;l=44-54

driver.Driver は 以下のinterface型です。

type Driver interface {
    Open(name string) (Conn, error)
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/database/sql/driver/driver.go;l=84-95

Openで返却されるdriver.Conn は以下のinterface型です。

type Conn interface {
    Prepare(query string) (Stmt, error)
    Close() error
    Begin() (Tx, error)
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/database/sql/driver/driver.go;l=230-251

さらに辿って、このPrepareで返却される driver.Stmt は以下のinterface型です。

type Stmt interface {
    Close() error
    NumInput() int
    Exec(args []Value) (Result, error)
    Query(args []Value) (Rows, error)
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/database/sql/driver/driver.go;l=326-358

このQueryが driver.Rows を返却することを覚えておいてください。

mysqlパッケージの例で整理すると、

  • 初めにinit関数の中で登録していた MySQLDriverdriver.Driver を満たす、つまり Open(string) (driver.Conn, error) メソッドを実装している
  • MySQLDriver.Opendriver.Conn を満たす mysqlConn 型を返す作りになっている
  • mysqlConn.Preparedriver.Stmt を満たす mysqlStmt 型を返す作りになっている

という実装になっています。
(省略していますが、各interfaceの残りのメソッドについても同様です。)

sqlパッケージのQuery処理を追う

DBインスタンスの生成(Open)

sql.Open は次の関数です。

func Open(driverName, dataSourceName string) (*DB, error) {
    // 略
    driveri, ok := drivers[driverName]
    // 略
    return OpenDB(dsnConnector{dsn: dataSourceName, driver: driveri}), nil
}

func OpenDB(c driver.Connector) *DB {
    // 略
    db := &DB{
        connector: c,
        // 略
    }
    // 略
    return db
}

https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/database/sql/sql.go;l=799-833

(「触れないこと」で記載の通り、ロックやコネクション周りのコードは省略し今回触れたい部分のみ抜粋しています。)

driversは先ほど触れた、sqlパッケージ直下に定義されているmapです。
つまり、Openによって我々がinit関数で追加したdriverを保持するDBインスタンスを得られたことがわかります。

DB.Queryメソッド

sqlxパッケージにはSelect関数やメソッドが定義されていますが、sqlパッケージにはSelectはなくQueryが定義されています。

// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) Query(query string, args ...any) (*Rows, error) {
    return db.QueryContext(context.Background(), query, args...)
}

https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/database/sql/sql.go;l=1722-1729

コメントに記載の通りこれがSELECTの実行を想定したメソッドとなっており、 sql.Rows を返却します。

sql.Rows の定義はこちらです(例によって着目したいフィールド以外は省略)。

type Rows struct {
    rowsi       driver.Rows
    // 略
}

https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/database/sql/sql.go;l=2916-2935

このフィールド rowsi driver.Rows に注目してください。 こちらが先ほど触れた、 driver.Stmt.Query メソッドで返却されていた値の型です。

実際に sql.DB.Query の中身を辿っていくと driver.Stmt.Query の値が詰められていることを見ていきましょう。

DB.QueryContext が実行され

func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*Rows, error) 

この内部では DB.query が実行されており、

func (db *DB) query(ctx context.Context, query string, args []any, strategy connReuseStrategy) (*Rows, error) 

この内部では DB.queryDC が実行されます。

func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []any) (*Rows, error) 

queryDCの第3引数で渡されている dc *driverConn はDB構造体から取得した構造体で、内部に driver.Conn フィールドを持ちます。
要するに、Openする際に渡したdriverのConnを保有しています。
(Open時にセットした db.connector フィールドから driver.Conn を取得する部分の解説は割愛しますが、コードは下記になります。)
https://cs.opensource.google/go/go/+/refs/tags/go1.17.8:src/database/sql/sql.go;l=1258-1383

DB.queryDC 内ではこの driver.Conndriver.QueryerContext インターフェースを満たすかどうかで分岐が入ります。 driver.QueryerContext の定義はこちらです。

type QueryerContext interface {
    QueryContext(ctx context.Context, query string, args []NamedValue) (Rows, error)
}

今回はひとまずシンプルなケースを追うことにして、渡した driver.Conn にこのメソッドが実装されていない場合の処理を見ていきましょう。
(この他にも、こうした「◯◯インターフェースを満たしていればそちらのメソッドを実行する」分岐は度々登場しますが、我々の driver.Conndriver.Stmt にはオプショナルなメソッドは何も実装されていないものとして追っていくことにします。)

DB.queryDC の中で以下の関数が実行され、

func ctxDriverPrepare(ctx context.Context, ci driver.Conn, query string) (driver.Stmt, error) {
    // 略
    si, err := ci.Prepare(query)
    // 略
    return si, err
}

と、冒頭で見た driver.Conn.Prepare が実行され driver.Stmt を得ます。

DB.queryDC に戻り、後続の処理を追うと以下の関数が実行され、

func ctxDriverStmtQuery(ctx context.Context, si driver.Stmt, nvdargs []driver.NamedValue) (driver.Rows, error) {
    // 略
    return si.Query(dargs)
}

これまた冒頭で見た driver.Stmt.Query が実行され driver.Rows を得ます。

あとは sql.Rowsdriver.Rows を包んで返却、といった流れになります。

長くなりましたがまとめると、DB.Queryの中身を追うと、(コネクション管理などのなんやかんやの処理に伴って)Open時に渡したdriverのConn, Stmtのメソッドを実行してその結果を返却している ことがわかりました!

まとめ

sqlパッケージのOpenからQueryの実行、そしてそこに渡すdriverパッケージの各種interfaceについてまとめました。
driver.Driver を満たす独自の型を定義し登録すればそれに従った sql.Rows を得ることはできるようです。
しかしsqlパッケージでは sql.Rows を返却するにとどまっており、これを任意の構造体に当てはめるといった処理は行なっていません。
次回はそれを行なっているsqlxパッケージを見ていく予定です。