読者です 読者をやめる 読者になる 読者になる

詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Go のシンプルかつ明快な SQL クエリビルダ go-sqlf

Go

Go でリレーショナルデータベースを利用したアプリケーションを書いているとき、動的に SQL を組み立てたい場合には、いくつかの方法が考えられます:

クエリビルダを使う。世の中にすでにいろいろ存在します。(そのためのライブラリなので)動的に生成するにはもってこいですが、この場合、それぞれのライブラリに合わせた書き方をしなければならないので読み手にもある程度負荷がある点、また、Go は言語として冗長に書くことをよしとする思想を持っているため、DSL 的な API との相性が悪いという欠点があります(map の組み立てが冗長、条件分岐する式が書けないなど)。また、一般にクエリビルダから生成される SQL がコードから想像しづらくなる問題もあります。

文字列連結や fmt.Sprintf を使う。発行される SQL は比較的分かりやすくなりますが、動的に組み立てると SQL プレースホルダとバインドされる値がソースコード中の離れた位置に登場するようになると、対応が分かりづらくなってしまいます。

自分も Go で RDB を使ったアプリケーションを書いていてこの点でかなり困ってしまったので、ひとつの解決案として、慣れ親しんだ fmt のインターフェースに乗っかったクエリビルダを書きました。

https://github.com/motemen/go-sqlf

使い方

sqlf.Printf(format string, values ...interface{}) sqlf.SQL がほぼ唯一の API です。返り値である sqlf.SQL が、発行したい(プレースホルダつきの)クエリと束縛する値の組を表します。さらにこの、BuildSQL() (string, []interface{}) メソッドで得られる queryargsdatabase/sql.DB.Query() などの API に渡すことで、望むクエリを実行できます。

query, args := sqlf.Printf(
    "SELECT %s FROM %s WHERE col1 = %_ AND col2 IN (%_)",
    "id",                   // SELECT %s
    "table",                // FROM %s
    "x",                    // col1 = %_
    []interface{}{1, 2, 3}, // col2 IN (%_)
).BuildSQL()

fmt.Println(query) // SELECT id FROM table WHERE col1 = ? AND col2 IN (?,?,?)
fmt.Println(args)  // [x 1 2 3]

見た目のとおり fmt のそのままのインターフェースですが、go-sqlf では特別に %_ というプレースホルダが定義されているのがポイントです。他と違って、これに対応する値はそのまま文字列には埋め込まれず、代わりに SQL のプレースホルダ(?)となります。値自身は BuildSQL した際の args のほうに出現します(上の例の "x")。

また特に値として []interface{} などスライスが与えられた場合、その個数に応じたプレースホルダが文字列に埋め込まれます(上の例の []interface{1,2,3})。WHERE ... IN (...) なクエリの生成に便利です。

BuildSQL() の返り値はそのまま database/sql の各種 API に渡すことを想定していて、これで意図したクエリを安全に実行できます。ショートカットとして、*sql.DB を引数にとって実行する Query といったメソッドを使うこともできます。

部分的な SQL の埋め込み

一般にクエリビルダを使う動機としては、例えば WHERE 節の一部の式を動的に変化させたいこともあります。go-sqlf ではこの場合、sqlf.Printf によって生成された sqlf.SQL を別の Printf の引数に渡すことで実現します。

wherePart := sqlf.Printf("col1 IN (%_)", []interface{}{"x", "y"})

query, args := sqlf.Printf(
    "SELECT id FROM table WHERE %_ AND col2 = %_",
    wherePart,
    "z",
).BuildSQL()

fmt.Println(query) // SELECT id FROM table WHERE col1 IN (?,?) AND col2 = ?
fmt.Println(args)  // [x y z]

wherePart のプレースホルダとその引数が最終的な結果にうまく利用されていることが分かると思います。

実装

fmt パッケージをそのまま利用しています(なので %s 以外にも例えば %d なんかもそのまま使えます)。fmt の API は引数が fmt.Formatter インターフェースを実装していればその実装を尊重して文字列展開するので、sqlf.Printf に渡された引数を適当な実装でくるんでやれば上記のような挙動も意外と簡単に実現できますし、元来の fmt では使われていない %_ も利用できるようです。

とはいえ、このへんはちゃんと実装や仕様を把握していないので、今後も有効かどうかは分からない。また、フォーマットが文字列の先頭から直列に処理されることに依存していますが、まあこの挙動は将来的にも変化ないんじゃないかな……。


以上、思いつきで書いたやつなので瑕疵もあるかと思いますが(MySQL のことしか考えられてないし)、けっこう便利そうなのでどうぞご利用ください。