Practice of Programming

プログラム とか Linuxとかの話題

GoのSQL Query Builder goquの紹介

GoでORMを使うのではなくて、SQLだけを作りたいという要望があって調べたところ、goquというものがみつかりましたので、試してみました。

書いてから気づきましたが、日本語の紹介記事は他にも結構ありました。

基本の使い方

Dialectを指定

MySQLPostgreSQLなどをDialectで指定できます。MySQLの場合は、以下のようにします。

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

上記のような importを行い、Dialectで指定します。

goqu.Dialect("mysql")

SELECTの使い方

Tableを指定する

続けて、Fromでテーブルを指定します。

goqu.Dialect("mysql").From("user")

SQLにする

最後に、ToSQL() で、SQLを受け取れます。

   sql, _, err := goqu.Dialect("mysql").From("user").ToSQL()
    fmt.Println(sql)

SQL:

SELECT * FROM `user`

ToSQL()SQLにするのではなく構造体にマッピングする

ScanStructsScanStruct がありますので、下記のように構造体にマッピングすることも出来ます。

type User struct{
  FirstName string `db:"first_name"`
  LastName  string `db:"last_name"`
  Age       int    `db:"-"` // a field that shouldn't be selected
}
   users := make([]Users, 0)
    sql, _, err := goqu.Dialect("mysql").From("user").ScanStructs(&users)

Selectで関数を使う

goqu.L を使うことで、任意の文字を渡すことができるので、関数を渡すことも出来ます。

   sql, _, err := goqu.Dialect("mysql").Select(goqu.L("AES_ENCRYPT(num, \"hoge\")").As("crypted")).From("user").ToSQL()
    fmt.Println(sql)

SQL:

SELECT AES_ENCRYPT(num, "hoge") AS `crypted` FROM `user`

Whereを指定する

Whereで条件を指定することも普通にできます。

   sql, _, err := goqu.Dialect("mysql").From("user").Where(
        goqu.Or(
            goqu.C("age").Gt(10),
            goqu.C("age").Lt(20),
        ),
    ).ToSQL()
    fmt.Println(sql)

SQL:

SELECT * FROM `user` WHERE ((`age` > 10) OR (`age` < 20))

このWhereの書き方は、下記のようにも書けます。

       goqu.Or(
            goqu.Ex{
                "age":  goqu.Op{"gt": 10},
            },
            goqu.Ex{
                "age": goqu.Op{"lt": 20},
            },
        ),

この書き方なら、カラムがかぶらなければ複数条件をまとめて書けます。

       goqu.Or(
            goqu.Ex{
                "age":  goqu.Op{"gt": 10},
                "name":  goqu.Op{"eq": "name"},
            },
            goqu.Ex{
                "age": goqu.Op{"lt": 20},
            },
        ),

まぁ、下記のようにも書けますし、このスタイルのほうが書きやすいし、読みやすいかなと思います。

       goqu.Or(
            goqu.And(
                goqu.C("age").Gt(10),
                goqu.C("name").Eq("name"),
            ),
            goqu.C("age").Lt(20),
        ),

Placeholderを使う

今までのものは、すべて、SQLの中に変数でわたし値が埋め込み済みでしたが、Prepared(true)を使うことで、Placeholderを使うことも出来ます。

   sql, args, err := goqu.Dialect("mysql").From("user").Where(
        goqu.Or(
            goqu.C("age").Gt(10),
            goqu.C("age").Lt(20),
        ),
    ).Prepared(true).ToSQL()
    fmt.Println(sql)
    // SELECT * FROM `user` WHERE ((`age` > ?) OR (`age` < ?))
    fmt.Printf("%#v\n", args)
    // []interface {}{10, 20}

もっと複雑なSQL

かなり複雑なSQLも表現可能です(複雑っぽくしたかっただけなので、出来上がるSQLには特に意味がないです)。

   ds := goqu.From("user_item").Select(goqu.COUNT("*")).GroupBy("user_id").As("ui")

    sql, _, _ = goqu.Dialect("mysql").
        Select(goqu.COUNT("*")).
        From("user").
        Join(
            goqu.T("user_hobby"),
            goqu.On(goqu.I("user.id").Eq(goqu.I("user_hobby.user_id"))),
        ).
        Join(
            ds,
            goqu.On(goqu.I("user.id").Eq(goqu.I("ui.user_id"))),
        ).
        Where(
            goqu.Or(
                goqu.And(
                    goqu.C("age").Gt(10),
                    goqu.C("name").Eq("name"),
                ),
                goqu.C("age").Lt(20),
            ),
        ).GroupBy("user.id").Having(goqu.SUM("income").Gt(1000)).ToSQL()

SQL:

SELECT COUNT(*) FROM `user`
   NNER JOIN `user_hobby`
        ON (`user`.`id` = `user_hobby`.`user_id`)
  INNER JOIN (SELECT COUNT(*) FROM "user_item" GROUP BY "user_id") AS `ui`
        ON (`user`.`id` = `ui`.`user_id`)
 WHERE (((`age` > 10) AND (`name` = 'name')) OR (`age` < 20))
 GROUP BY `user`.`id`  HAVING (SUM(`income`) > 1000)

その他

詳しくは、ドキュメントを見てもらえれば良いのですが、希望するものはだいたいできるんじゃないのかなぁという印象を持ちました。SELECT FOR UPDATE なんかも使えるようです。

github.com

Updateの使い方

WhereはSELECTのときと同じなので省略します。Updateでテーブル名を渡して、Setでデータをセットします。

   sql, _, _ = goqu.Dialect("mysql").Update("user").Set(goqu.Record{"name": "name1"}).ToSQL()
    fmt.Println(sql)

SQL:

UPDATE `user` SET `name`='name1'

DELETの使い方

こちらも、WhereはSELECTのときと同じなので省略します。Deleteにテーブル名を渡します。

   sql, _, _ = goqu.Dialect("mysql").Delete("user").ToSQL()
    fmt.Println(sql)

SQL:

DELETE `user` FROM `user`

DELETEの後ろにテーブル名入れて使った覚えがあまりないんですが、正しいSQLでした。

INSERTの使い方

INSERTは、goqu.Recordでレコードのセットを作って、Rowsに渡します。

   users := []goqu.Record{
        {"first_name": "Bob", "last_name": "Yukon", "created": time.Now()},
        {"first_name": "Sally", "last_name": "Yukon", "created": time.Now()},
        {"first_name": "Jimmy", "last_name": "Yukon", "created": time.Now()},
    }
    sql, _, _ = goqu.Dialect("mysql").Insert("user").Rows(users).ToSQL()
    fmt.Println(sql)
 INSERT INTO `user` (`created`, `first_name`, `last_name`) 
VALUES ('2024-08-19 00:29:07', 'Bob', 'Yukon'),
               ('2024-08-19 00:29:07', 'Sally', 'Yukon'),
               ('2024-08-19 00:29:07', 'Jimmy', 'Yukon')

ColsVals でわけて指定したり、Rows に構造体を渡すこともできるようです。

終わり

ということで、一通りgoquの使い方を説明しました。

途中でも紹介しましたが、下記に、それぞれのオペレーションについてのドキュメントが詳しく載っています。

github.com

デメリットとしては、テーブル名や、カラム名をハードコーディングしないといけないところですが、そこに関しては、コードを自動生成することでなんとかできそうですね。

次回に続きます。