フューチャー技術ブログ

SQLビルダーgoquの使い方

この記事はORMとクエリビルダー連載の6記事目です。

TIGの伊藤真彦です。業務では専らDynamoDBに触れており、1年以上RDBMSの世界から離れていたのですが、連載のために帰ってきました。というわけで無からネタを考え、goqu体験記を書くことにしました。

goquとは

リポジトリはこちらです。

goqu is an expressive SQL builder and executor
goquは、表現力豊かなSQLビルダーおよびエグゼキューターです。

との説明の通り、SQLビルダーとして単独に機能しつつ、SQLの実行も可能なライブラリです。

どちらかというとSQLビルダーとしての役割に注力しており、アソシエーションやフックを利用したO/Rマッパーとしての機能を求めている場合はgormhoodの使用がREADMEでも推奨されています。

While goqu may support the scanning of rows into structs it is not intended to be used as an ORM if you are looking for common ORM features like associations, or hooks I would recommend looking at some of the great ORM libraries such as:

  • gorm

  • hood

goquは行の構造体へのスキャンをサポートしている場合がありますが、関連付けやフックなどの一般的なORM機能を探している場合は、ORMとして使用することを意図していません。次のような優れたORMライブラリのいくつかを調べることをお勧めします。

その上でgoquを開発した意図もREADMEに書いてあります。

We tried a few other sql builders but each was a thin wrapper around sql fragments that we found error prone. goqu was built with the following goals in mind:

Make the generation of SQL easy and enjoyable
Create an expressive DSL that would find common errors with SQL at compile time.
Provide a DSL that accounts for the common SQL expressions, NOT every nuance for each database.
Provide developers the ability to:
Use SQL when desired
Easily scan results into primitive values and structs
Use the native sql.Db methods when desired

他のいくつかのSQLビルダーを試しましたが、それぞれがエラーが発生しやすいSQLフラグメントの薄いラッパーでした。goqu次の目標を念頭に置いて構築されました。

SQLの生成を簡単で楽しいものにします
コンパイル時にSQLで一般的なエラーを見つける表現力豊かなDSLを作成します。
各データベースのすべてのニュアンスではなく、一般的なSQL式を説明するDSLを提供します。
開発者に次の機能を提供します。
必要に応じてSQLを使用する
結果を簡単にスキャンしてプリミティブ値と構造体にする
必要に応じて、ネイティブのsql.Dbメソッドを使用します

私は残念ながら仕事でgormなどGo製O/Rマッパーをゴリゴリ使い込むようなことをしていないため、作者のdoug-martin氏への共感を持てる経験は持っていないのですが、好評な意見は各所で聞いています。

動作確認ついでにちょっとしたコメント修正のPRを送ってみましたが、次の日にはマージされました、活発にメンテナンスされているようです。

触ってみた

SQLを発行する

main.go
package main

import (
"fmt"
"log"
"github.com/doug-martin/goqu/v9"
)

func main() {
sql, _, err := goqu.From("test").ToSQL()
if err != nil {
log.Fatal(err)
}
fmt.Println(sql) // -> SELECT * FROM "test"
}

importしてサッと使うだけでSQLが文字列として取得できます。

Goには”database/sql”パッケージがあるので、素朴にそれと組み合わせるか、O/Rマッパーの痒いところに手が届かない部分をgoquで補うような使い方ができます。

上記で使っていないToSQLの戻り値は実装を見ると[]interface{}型の変数argsが帰ってきています。

これはSQLの表現の方法を変えたいときに利用できます。

READMEでParameter interpolationとして説明されている機能ですね。

テストコードに良いサンプルがあります。.Prepared(true)を付与することでパラメータ部分をargsに分離して使う前提のSQLが発行されます。

test.go
func ExampleDialect_datasetMysql() {
// import _ "github.com/doug-martin/goqu/v9/adapters/mysql"

d := goqu.Dialect("mysql")
ds := d.From("test").Where(goqu.Ex{
"foo": "bar",
"baz": []int64{1, 2, 3},
}).Limit(10)

sql, args, _ := ds.ToSQL()
fmt.Println(sql, args)

sql, args, _ = ds.Prepared(true).ToSQL()
fmt.Println(sql, args)

// Output:
// SELECT * FROM `test` WHERE ((`baz` IN (1, 2, 3)) AND (`foo` = 'bar')) LIMIT 10 []
// SELECT * FROM `test` WHERE ((`baz` IN (?, ?, ?)) AND (`foo` = ?)) LIMIT ? [1 2 3 bar 10]
}

Where句の利用やInsert文の場合など、細かい利用方法はREADMEに記載がありますので割愛します。

特定のDBに適したSQLを発行する

goquではDialectと称して、DB毎の細かな違いをドライバを切り替えるような機能が存在します。

PostgreSQLの場合を見てみます。

main.go
import (
"fmt"
"github.com/doug-martin/goqu/v9"
// import the dialect
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
)

// look up the dialect
dialect := goqu.Dialect("postgres")

// use dialect.From to get a dataset to build your SQL
ds := dialect.From("test").Where(goqu.Ex{"id": 10})
sql, args, err := ds.ToSQL()
if err != nil{
fmt.Println("An error occurred while generating the SQL", err.Error())
}else{
fmt.Println(sql, args)
}
SELECT * FROM "test" WHERE "id" = 10 []

続いてMySQLの場合です。

main.go
import (
"fmt"
"github.com/doug-martin/goqu/v9"
// import the dialect
_ "github.com/doug-martin/goqu/v9/dialect/mysql"
)

// look up the dialect
dialect := goqu.Dialect("mysql")

// use dialect.From to get a dataset to build your SQL
ds := dialect.From("test").Where(goqu.Ex{"id": 10})
sql, args, err := ds.ToSQL()
if err != nil{
fmt.Println("An error occurred while generating the SQL", err.Error())
}else{
fmt.Println(sql, args)
}

SQLの内容が変わりました。

SELECT * FROM `test` WHERE `id` = 10 []

詳しくはドキュメントに記載があります。

SQLを実行する

O/Rマッパー的な使い方が推奨されていない手前もあるのか、トップページのREADMEではあまり触れられていませんが、”database/sql”を利用したDBクライアントを組み込んで、goquのメソッドチェーンでSQLを実行したり、取得した結果を構造体にマッピングすることも可能です。
下記のコードがテーブルに書込みを行い、読み取り結果を構造体にマッピングするコードです。

main.go
package main

import (
"database/sql"
"fmt"
"log"

"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
_ "github.com/lib/pq"
)

type Record struct {
Id []uint8
Name string
}

func main() {
sqlDb, err := sql.Open("postgres", "user=postgres dbname=goqutest sslmode=disable ")
if err != nil {
panic(err.Error())
}
db := goqu.New("postgres", sqlDb)

// Write
insert:= db.Insert("test").Rows(
goqu.Record{"id": "1", "name": "test"},
// Record{Id: []uint8{49, 32, 32, 32}, Name: "test"},でも可
).Executor()

if _, err = insert.Exec(); err != nil {
log.Fatal(err)
}

// Read
var records []Record
err = db.From("test").ScanStructs(&records)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v\n", records) // -> [{[49 32 32 32] test}]
}

実際に試してみたい方のために動作をサクッと確認するための手順も説明します。

手軽に試すためにdockerでPostgreSQLを起動します。
MySQLにしなかったのは発掘したサンプルコードがたまたまPostgreSQLだったからです、どちらも好きです。

docker run --rm \
-p 5432:5432 \
-v postgres-tmp:/var/lib/postgresql/data \
-e POSTGRES_HOST_AUTH_METHOD=trust \
postgres:12-alpine

PostgreSQLのクライアントで起動したDBにログインします

psql -h localhost -p 5432 -U postgres

psqlコマンドが実行できない場合はPostgreSQLのインストールが必要です

ログインできたらテストで使うDB、テーブルを用意します。

データベース作成
CREATE DATABASE goqutest;

作成したデータベースに切り替え
\c goqutest

テーブル作成
CREATE TABLE test
(id char(4) not null,
name text not null,
PRIMARY KEY(id));

テーブル確認
\d

この状態でサンプルコードを実行すれば動くはずです。[]uint8になると思っていなかったのでテストのためのDBでidをchar(4)にしたのは失敗でした。

ScanStructsなどのマッピング系の機能はドキュメントのSelectingに、Insert機能の詳しい使い方はInsertingに記載があります。

これらのRead, WriteはContextとの組み合わせにも対応しています

main.go
package main

import (
"context"
"database/sql"
"fmt"
"log"

"github.com/doug-martin/goqu/v9"
_ "github.com/doug-martin/goqu/v9/dialect/postgres"
_ "github.com/lib/pq"
)

type Record struct {
Id []uint8
Name string
}

func main() {
sqlDb, err := sql.Open("postgres", "user=postgres dbname=goqutest sslmode=disable ")
if err != nil {
panic(err.Error())
}
db := goqu.New("postgres", sqlDb)

// Write
insert := db.Insert("test").Rows(
goqu.Record{"id": "1", "name": "test"},
// Record{Id: []uint8{49, 32, 32, 32}, Name: "test"},でも可
).Executor()

ctx := context.TODO()

if _, err = insert.ExecContext(ctx); err != nil {
log.Fatal(err)
}

// Read
var records []Record
err = db.From("test").ScanStructsContext(ctx, &records)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v\n", records) // -> [{[49 32 32 32] test}]
}

ToSQLの戻り値を使わなくても、メソッドチェーンの流れでExecutorを利用すればそのままSQLの実行までできるため、わざわざ使うかというと微妙ですが、Parameter interpolationをこのように利用してデータを読み取ることもできます。

main.go
sql, args, _ := db.From("test").Prepared(true).Where(goqu.Ex{"name": "test"}).ToSQL()
db.ScanStructs(&records, sql, args...)

まとめ

goquはSQLビルダーとしての役割に軸足を置いたライブラリである。汎用的なものから特定のDBに特化したものまでSQLを発行できる。O/Rマッパーとしての利用を推奨していないながらも単体で動作することが可能になっている。

どのライブラリが好みに合うかは人それぞれですが、触ってみた感触としては多機能ながらも学習障壁の高さを感じる部分は少なかったです。

とはいえ軽く触った程度ではまだまだ紹介するべき本領を発揮できていないんじゃないかと感じる程度には機能が豊富でした。

一部の機能が刺さるユースケースには勿論ですが、ひとまずこれを使ってみるという用途としても充分オススメできると感じました。

明日は本田さんのGo言語で2WaySQLです。