Yappli Tech Blog

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

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

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

前回の記事に続いて、今回は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.DBsql.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.Rowssqlx.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を満たす独自の型さえ実装すればやりたいことが実現できそうです!