サーバーサイドエンジニアの森谷です。
前回の記事に続いて、今回はsqlxパッケージについて見ていこうと思います。
簡単なおさらい
前回はdriverパッケージとsqlパッケージを読みました。
driver.Driver
インターフェースを満たす型を実装し sql.Register
でsqlパッケージに登録すると、 sql.Query
実行時にこのDriverに定義した処理に従った sql.Rows
を得られることがわかりました。
しかし sql.Query
で実現できる処理はここまでで、例えばSELECT結果を自前のUser構造体の各フィールドに当てはめるといった処理はこれ単体ではできません。
その処理を行なってくれるsqlxパッケージを今回は見ていきます。
sqlパッケージのScanとScannerについて
sqlxパッケージの説明に入る前に、まずこれらについて見ていこうと思います。
sql.Query
単体では任意の型に値を詰めることは出来ないことを述べましたが、実現する方法自体は提供されています。
それが Rows.Scan
メソッドと Scanner
インターフェースです。
func (rs *Rows) Scan(dest ...any) error type Scanner interface { Scan(src any) error }
https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/database/sql/sql.go;l=3206-3293 https://cs.opensource.google/go/go/+/refs/tags/go1.18:src/database/sql/sql.go;l=395-416
Rows.Scan
は対象のカラムの数だけdestを渡し、
- Rowsごと(各レコードごと)・各カラムごとにループし
- それぞれの値がScan可能であればdestに詰める
ことを行なっています。
(ちなみに、anyはGo1.18で導入された interface{}
の型エイリアスです。既にsqlパッケージにはこのように1.18で新登場したコードが現れています。)
この「Scan可能」の条件は大きく2つあり、1つ目は
- srcが
string
でdestが*string
- srcが
string
でdestが*[]byte
- srcが
time.Time
でdestが*time.Time
- etc...
といった具合に、あらかじめsqlパッケージ内で実装されているsrc, destの型の組み合わせであることです。
string, []byte, bool, int系, float系など基本的な型はサポートされています。
(詳しくはScanのドキュメントもしくはswitch caseがゴリゴリに書かれているこちらのソースを参照ください。)
条件の2つ目は、destで渡した型が Scanner
インターフェースを実装していることです。
上記で網羅されていない型の場合、次点の処理として
if scanner, ok := dest.(Scanner); ok { return scanner.Scan(src) }
が実行されます。
前者の一覧に構造体は含まれていないため、たとえば「json型のカラムを構造体に変換したい」といった場合には、その構造体に対し自前でScanメソッドを実装する必要があります。
また前述の通り、そもそもこのsqlパッケージのScanはカラムの数だけdestを渡さなければなりません。つまり、
rows.Scan(user.ID, user.Name, user.Email, user.CreatedAt, user.UpdatedAt)
のようなコードを書く必要があります。
(念の為補足しますと、こちらはレコード1行に対するScanですので、複数行に対して実行するならば for rows.Next() {...}
で包む必要もあります。)
こういった面倒な部分を担ってくれているのがsqlxパッケージです。
ということで、sqlxパッケージの中身を見ていきましょう。
Open
今回もOpenから処理を見ていきましょう。
と言っても sqlx.Open
は実にシンプルで、ほぼ sql.Open
のラッパーです。
返却される sqlx.DB
型も sql.DB
をラップした構造体になります。
func Open(driverName, dataSourceName string) (*DB, error) { db, err := sql.Open(driverName, dataSourceName) if err != nil { return nil, err } return &DB{DB: db, driverName: driverName, Mapper: mapper()}, err } type DB struct { *sql.DB // sql.DBのラッパーであることに注目 driverName string unsafe bool Mapper *reflectx.Mapper }
https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L261-L267 https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L240-L247
Select
DB.Select
の処理は以下になります。
func (db *DB) Select(dest interface{}, query string, args ...interface{}) error { return Select(db, dest, query, args...) } func Select(q Queryer, dest interface{}, query string, args ...interface{}) error { rows, err := q.Queryx(query, args...) if err != nil { return err } defer rows.Close() return scanAll(rows, dest, false) }
https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L314-L318 https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L668-L681
ざっくりSelectの仕事をまとめると、
- 受け取った
Queryer
インターフェースに従ってqueryを実行してRows
型を構築し - dest(これはsliceであるべき引数です)に対しscan処理を実行し値を嵌め込む
の2つです。
順に見ていきましょう。
Queryの実行とRowsの構築
Select関数の方の第1引数では Queryer
を受け取る作りになっています。
これは以下のinterfaceです。
type Queryer interface { Query(query string, args ...interface{}) (*sql.Rows, error) Queryx(query string, args ...interface{}) (*Rows, error) QueryRowx(query string, args ...interface{}) *Row } type Rows struct { *sql.Rows // sql.Rowsのラッパーであることに注目 unsafe bool Mapper *reflectx.Mapper started bool fields [][]int values []interface{} } type Row struct { err error unsafe bool rows *sql.Rows // sql.Rowsのラッパーであることに注目 Mapper *reflectx.Mapper }
https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L77-L82 https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L572-L582 https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L165-L172
DB型がこのQueryerを満たしていることを確認しておきましょう。
まずQueryについてですが、これは sql.DB
が持っているメソッドです。
package sql 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.17.8:src/database/sql/sql.go;l=1685-1692
sqlx.DB
は sql.DB
を埋め込みで保有していることを思い出してください。
よって、sqlx.DB
自体もQueryメソッドを持っているものとして振る舞うことができます。
Queryx, QueryRowxについてはxのサフィックスがついていることからわかるように、sqlxパッケージ側で定義されているメソッドになります。
func (db *DB) Queryx(query string, args ...interface{}) (*Rows, error) { r, err := db.DB.Query(query, args...) if err != nil { return nil, err } return &Rows{Rows: r, unsafe: db.unsafe, Mapper: db.Mapper}, err } func (db *DB) QueryRowx(query string, args ...interface{}) *Row { rows, err := db.DB.Query(query, args...) return &Row{rows: rows, err: err, unsafe: db.unsafe, Mapper: db.Mapper} }
https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L346-L361
これらも sqlx.Open
のように、 sql.Query
のラッパーかつ sql.Rows
をラップした sqlx.Rows
や sqlx.Row
を返却するメソッドになっています。
つまるところ、 DBドライバー由来のQueryメソッドを実行し、諸々の情報を加えた上でラップした構造体を返す ということを行なっているだけです。
Rowsをdestにscanする
この部分がsqlxパッケージの恩恵が大きい箇所の1つです。
Selectの最後で呼ばれていたscanAllを見ましょう。
ここのコードはreflectがふんだんに使用されたハードボイルドな内容になっているので、大幅に省略しつつコード中にコメントを差し込んで解説します。
// scanAllの第1引数が rowsi インターフェースになっていることや // 第3引数に structOnly というbooleanを受け取っている理由は、 // このscanAllが別の関数からも呼ばれているためです。 // が、今は第1引数に *Rows 型が渡されていることだけ理解し、 // さらに第3引数は無視して読み進めていただいて構いません。 func scanAll(rows rowsi, dest interface{}, structOnly bool) error { // reflectでゴニョゴニョ頑張っている部分。略。 // baseはdestが何の型のsliceかという情報(reflect.Type)。 // isScannableはboolを返し、baseが // * 構造体でないか // * sql.Scannerを実装しているか // * 公開フィールドを持っていない // 場合にtrueを返します。 scannable := isScannable(base) // いくつかのエラーチェック処理。略。 // scannableではない場合、つまりScannerを実装しておらず公開フィールドをもつ構造体の場合。 if !scannable { // reflectでゴニョゴニョ頑張ったりunsafeのケアを頑張っている部分。略。 for rows.Next() { // reflectでゴニョゴニョ頑張ってvalues []interface{} を整えている部分。略。 // sqlパッケージに定義されているScanを実行。 // ここが、 // `rows.Scan(user.ID, user.Name, user.Email)のようなコードを書かなければなりません` // と前述した部分の面倒臭さを肩代わりしてくれている箇所です! err = rows.Scan(values...) if err != nil { return err } // directは reflect.Indirect(reflect.ValueOf(dest))。 // 要するにdestのsliceにappendしているだけのコードです。 if isPtr { direct.Set(reflect.Append(direct, vp)) } else { direct.Set(reflect.Append(direct, v)) } } // scannableな場合。 } else { for rows.Next() { // baseがScannerインターフェースを満たしていれば、sqlパッケージ側でそれを用いてくれる。 vp = reflect.New(base) err = rows.Scan(vp.Interface()) if err != nil { return err } // destのsliceにappend if isPtr { direct.Set(reflect.Append(direct, vp)) } else { direct.Set(reflect.Append(direct, reflect.Indirect(vp))) } } } return rows.Err() }
https://github.com/jmoiron/sqlx/blob/92bfa368c21aafd33626444a47b2f99be2432623/sqlx.go#L880-L989
まとめ
前回の記事とあわせて、
driver.Driver
インターフェースとして何を実装すればsqlパッケージに登録できるのか- sqlパッケージでのDriverの扱い方
sql.Scanner
インターフェースで独自のScan処理を反映させられること- sqlxパッケージにおけるScan処理の拡張
を見てきました。
これまで雰囲気でsqlxパッケージを利用していたので、それぞれのパッケージの仕事内容を整理できたのは面白かったです。
(Scanメソッド自体は業務コードで見知ってはいたのですが、sqlパッケージで定義されているinterfaceだとは知らず衝撃でした。)
おまけ
もはやsql, sqlxパッケージを調べようとした動機を忘れかけていますが、発端は「SELECT結果をモックしてScanの挙動を色々検証したい」ということでした。
sqlパッケージだけを読んでいた時点では、「わざわざdriverを実装しないと難しいのかな...」と思っていましたが、
func Select(q Queryer, dest interface{}, query string, args ...interface{}) error
を実行すれば良いことがわかり、つまり単にQueryerを満たす独自の型さえ実装すればやりたいことが実現できそうです!