サーバーサイドエンジニアの森谷です。
弊社では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{}) }
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関数の中で登録していた
MySQLDriver
はdriver.Driver
を満たす、つまりOpen(string) (driver.Conn, error)
メソッドを実装している MySQLDriver.Open
はdriver.Conn
を満たすmysqlConn
型を返す作りになっているmysqlConn.Prepare
はdriver.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.Conn
が driver.QueryerContext
インターフェースを満たすかどうかで分岐が入ります。
driver.QueryerContext
の定義はこちらです。
type QueryerContext interface { QueryContext(ctx context.Context, query string, args []NamedValue) (Rows, error) }
今回はひとまずシンプルなケースを追うことにして、渡した driver.Conn
にこのメソッドが実装されていない場合の処理を見ていきましょう。
(この他にも、こうした「◯◯インターフェースを満たしていればそちらのメソッドを実行する」分岐は度々登場しますが、我々の driver.Conn
や driver.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.Rows
で driver.Rows
を包んで返却、といった流れになります。
長くなりましたがまとめると、DB.Queryの中身を追うと、(コネクション管理などのなんやかんやの処理に伴って)Open時に渡したdriverのConn, Stmtのメソッドを実行してその結果を返却している ことがわかりました!
まとめ
sqlパッケージのOpenからQueryの実行、そしてそこに渡すdriverパッケージの各種interfaceについてまとめました。
driver.Driver
を満たす独自の型を定義し登録すればそれに従った sql.Rows
を得ることはできるようです。
しかしsqlパッケージでは sql.Rows
を返却するにとどまっており、これを任意の構造体に当てはめるといった処理は行なっていません。
次回はそれを行なっているsqlxパッケージを見ていく予定です。