emahiro/b.log

日々の勉強の記録とか育児の記録とか。

squirrel という Go のクエリビルダーが便利

Overview

ライブラリの紹介記事です。

squirrel という Go の クエリビルダーが便利だったのでその紹介です。

github.com

何をしてくれるのか

ORM 的にメソッドチェーンでクエリを動的に組み立ててくれます。

実際に使うときは README を参照してクエリを組み立ててください。最後に ToSql() を呼ぶと SQL と Placeholder に指定した値の slice (interface{} 型だけど)が出力されます。
以下はサンプルです。

Sample1

import sq "github.com/Masterminds/squirrel"

query, args, err := sq.Select("*").From("users").Where(sq.Eq{"id": 1}).ToSql()

// select * from user where id = 1;

Sample2

import sq "github.com/Masterminds/squirrel"

query, args, err := sq.Select("*").From("users").Where(
    sq.And{
        sq.Eq{"gender":  "male"},
        sq.Eq{"age": 10},
    },
).ToSql()

// select * from user where gender = "male" and age = 10;

クエリを組み立てる "だけ” の機能を提供してくれるので以下のような効用があると思います。

  • database/sql もしくは sqlx でクエリを実行するときに生のSQLを書くのがめんどくさいとき。 (*1)
  • ユースケースに応じて動的に出力されるクエリを変更したいとき。

*1. クエリを文字列で組み立てるケースで十分だとも思いますが、スペースの取り扱いをミスってクエリエラーが発生したり、クエリのフォーマットが個々人で書き方バラバラになったりといったストレスはこういったクエリビルダーを使った方が低減されると思います。

何をしてくれないのか

クエリを組み立てるだけなので、実際に DB と接続して SQL を叩くのは別のライブラリを使う必要があります。
※ ただ、それがこのライブラリの良さだと考えています。

ハマったこと

In が明示的に用意されていない

SQL の In 句がクエリビルダーに用意されてないのかと思って以下の issue を参考に In のヘルパー関数を用意しようかと思いましが、

github.com

以下のような書き方で In 句が出力されることを教えてもらいました。

query, args, err := sq.Select("*").From("users").Where(sq.Eq{"id": ids}).ToSql()

まとめ

依存が少ない状態で、ユースケースに応じて動的なクエリを変更したいケースでは愚直に文字列を繋いでくことを是として考えましたが、こう言ってクエリビルダーのみの機能を提供してくれるライブラリを知りました。他にもあると思いますが、 squirrel は提供してくれている機能もシンプルでいいライブラリだなと思います。

余談

カラムを全て取得する

squirrel と関係ないですが、SQL を発行する際にすべてのカラムを select クエリに書く(= ワイルドカードをつかない)のがめんどくさいので、struct のフィールドに指定したマッピング用の付加情報タグから、カラム情報を取得する実装をメモ程度に残しておきます。

type User struct {
    ID   int64  `$TagName:"id"`
    Name string `$TagName:"name"`
}

func main() {
    user := User{}
    t := reflect.TypeOf(user)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("%v\n", field.Tag.Get("$TagName"))
    }
}

https://go.dev/play/p/8gjhDrGQrD7 は sqlx でマッピング用のフィールドには db タグを使うので db タグの付いたカラム名一覧を取り出す実装です。毎回 reflect が必要なのがちょっとネックなポイントだなぁとは思っています。

追記

ちなみにワイルドカードをカラム指定とreflect によるカラム取得をベンチしてみたところワイルドカードが単純なパフォーマンスは一番いいことがわかりました。これはおそらく colums に渡す slice の大きさに比例していそうです。

$ go test --bench . --benchmem
goos: darwin
goarch: amd64
pkg: sample.com/go-sandbox
cpu: VirtualApple @ 2.50GHz
Benchmark_WildCard-10             484780              2097 ns/op            1688 B/op         33 allocs/op
Benchmark_PlainColumns-10         386214              2869 ns/op            2240 B/op         45 allocs/op
Benchmark_Columns-10              316792              3630 ns/op            2776 B/op         56 allocs/op

コードは以下

package main

import (
    "reflect"
    "testing"
    "time"

    sq "github.com/Masterminds/squirrel"
)

type User struct {
    ID        int64     `db:"id"`
    Name      string    `db:"name"`
    Age       int64     `db:"age"`
    CreatedAt time.Time `db:"created_at"`
    UpdatedAt time.Time `db:"updated_at"`
}

func (u User) Columns() []string {
    var columns []string
    user := User{}
    t := reflect.TypeOf(user)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        columns = append(columns, field.Tag.Get("db"))
    }
    return columns
}

func Benchmark_WildCard(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sq.Select("*").
            From("`user`").ToSql()
    }
}

func Benchmark_PlainColumns(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sq.Select(`id`, `name`, `age`, `created_at`, `updated_at`).
            From("`user`").ToSql()
    }
}

func Benchmark_Columns(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        sq.Select(User{}.Columns()...).
            From("`user_concierge_counseling`").ToSql()
    }
}

余談ですが、暗黙的なカラムと言う話もあるのでワイルドカードをそもそも使うのが正しいのか、と言うのはユースケースに依存すると思います。