カテゴリー: BackEnd

Goのクエリビルダー goqu を使ってみる

はじめに

最近携わっているとある案件では、CRUD操作はsqlcで生成したORMを使い、QueryServiceではgoquを使ってクエリを組み立てています。

以前の記事ではsqlcについて紹介しましたので、今回はgoquについて紹介したいと思います。

goquとは

まず初めに、goquについて簡単に紹介します。

goquは複数のDBに対応した多機能のクエリビルダーです。

goquで提供されている様々なExpressionsなどを利用して、複雑なクエリを楽に楽しく記述することができます。

また、クエリを組み立てるだけでなく、直接クエリを実行することもできます。

更に、複数のレコードをスキャンして、構造体やプリミティブにマッピングすることもできます。ただし、goquはORMとして使用されることを想定してるわけではないので、アソシエーションやフックといった機能は提供していません。

対応するDB

投稿時点では、goquは以下のDBのクエリ生成に対応しています。

  • MySQL
  • PostgreSQL
  • SQLite3
  • SQL Server

また、これら以外の書式であっても、Custom Dialectsを定義することで対応することができます。

基本的な使い方

各クエリ操作を行う際にはまず、特定のDBの書式にあったクエリを出力するために、Dialect関数を使用してDiralectWrapperを生成します。

package main

import (
 "fmt"

 "github.com/doug-martin/goqu/v9"
 // Import the mysql dialect
 _ "github.com/doug-martin/goqu/v9/dialect/mysql"
)

func main() {
 dialect := goqu.Dialect("mysql")

 ds := dialect.From("users")
 sql, args, err := ds.ToSQL()
 if err != nil {
  panic(err)
 }

 fmt.Printf("SQL: %s, Args: %v\n", sql, args)
 // SQL: SELECT * FROM `users`, Args: []
}

注意点としては、使用したいDBに対応するパッケージをインポートしておく必要があります。

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

ちなみに、Dialectを使わないでクエリを生成する方法もありますが、便宜上、本記事では全てDialectを使ってクエリを生成します。

Insert句を生成する

Insert句を生成するには、生成したDialectのInsert()メソッドにCols()メソッドとVals()メソッドをチェインする形で行います。

package main

import (
 "fmt"

 "github.com/doug-martin/goqu/v9"
 // Import the mysql dialect
 _ "github.com/doug-martin/goqu/v9/dialect/mysql"
)

func main() {
 dialect := goqu.Dialect("mysql")

 ds := dialect.From("users").Insert().
  Cols("first_name", "last_name").
  Vals(goqu.Vals{"John", "Doe"})
 sql, args, err := ds.ToSQL()
 if err != nil {
  panic(err)
 }

 fmt.Printf("SQL: %s, Args: %v\n", sql, args)
 // SQL: INSERT INTO `users` (`first_name`, `last_name`) VALUES ('John', 'Doe'), Args: []
}

または、Rows()メソッドをチェインする書き方もできます。

ds := dialect.From("users").Insert().Rows(
 goqu.Record{
  "first_name": "John",
  "last_name":  "Doe",
 },
)
sql, args, err := ds.ToSQL()
if err != nil {
 panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: INSERT INTO `users` (`first_name`, `last_name`) VALUES ('John', 'Doe'), Args: []

すでに構造体が定義されている場合は、それを使うこともできます。

type User struct {
 FirstName string
 LastName  string `db:"last_name"` // タグを使ってカラム名を指定
}

ds := dialect.From("users").Insert().
 Rows(User{FirstName: "John", LastName: "Doe"})
sql, args, err := ds.ToSQL()
if err != nil {
 panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: INSERT INTO `users` (`firstname`, `last_name`) VALUES ('John', 'Doe'), Args: []

Select句を生成する

Select句を生成するには、Select()メソッドと必要に応じてWhere()メソッドなどをチェインして行います。

package main

import (
 "fmt"

 "github.com/doug-martin/goqu/v9"
 // Import the mysql dialect
 _ "github.com/doug-martin/goqu/v9/dialect/mysql"
)

func main() {
 dialect := goqu.Dialect("mysql")

 ds := dialect.From("users").
  Select("first_name").
  Where(goqu.Ex{"id": 1})
 sql, args, err := ds.ToSQL()
 if err != nil {
  panic(err)
 }

 fmt.Printf("SQL: %s, Args: %v\n", sql, args)
 // SQL: SELECT `first_name` FROM `users` WHERE (`id` = 1), Args: []
}

COUNTやMAXなどの関数も使えます。

ds := dialect.From("users").
Select(
 goqu.COUNT("id").As("count"),
 goqu.MAX("age").As("max_age"),
 goqu.AVG("age").As("avg_age"),
)
sql, args, err := ds.ToSQL()
if err != nil {
panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT COUNT(`id`) AS `count`, MAX(`age`) AS `max_age`, AVG(`age`) AS `avg_age` FROM `users`, Args: []

すでに構造体が定義されている場合は、それを使うこともできます。

type User struct {
 FirstName string `db:"first_name"`
 LastName  string `db:"last_name"`
}
ds := dialect.From("users").Select(&User{})
sql, args, err := ds.ToSQL()
if err != nil {
 panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT `first_name`, `last_name` FROM `users`, Args: []

Update句を生成する

Update句を生成するには、Update()メソッドとSet()メソッドなどをチェインして行います。

package main

import (
 "fmt"

 "github.com/doug-martin/goqu/v9"
 // Import the mysql dialect
 _ "github.com/doug-martin/goqu/v9/dialect/mysql"
)

func main() {
 dialect := goqu.Dialect("mysql")

 ds := dialect.Update("users").
  Set(goqu.Record{
   "first_name": "John",
   "last_name":  "Doe",
  }).
  Where(goqu.Ex{"id": 1})
 sql, args, err := ds.ToSQL()
 if err != nil {
  panic(err)
 }

 fmt.Printf("SQL: %s, Args: %v\n", sql, args)
 // SQL: UPDATE `users` SET `first_name`='John',`last_name`='Doe' WHERE (`id` = 1), Args: []
}

すでに構造体が定義されている場合は、それを使うこともできます。

type user struct {
 FirstName string `db:"first_name"`
 LastName  string `db:"last_name"`
}
ds := dialect.Update("users").
 Set(user{FirstName: "John", LastName: "Doe"}).
 Where(goqu.Ex{"id": 1})
sql, args, err := ds.ToSQL()
if err != nil {
 panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: UPDATE `users` SET `first_name`='John',`last_name`='Doe' WHERE (`id` = 1), Args: []

Delete句を生成する

Delete句を生成するには、Delete()メソッドをチェインして行います。

package main

import (
 "fmt"

 "github.com/doug-martin/goqu/v9"
 // Import the mysql dialect
 _ "github.com/doug-martin/goqu/v9/dialect/mysql"
)

func main() {
 dialect := goqu.Dialect("mysql")

 ds := dialect.Delete("users").Where(goqu.Ex{"id": 1})
 sql, args, err := ds.ToSQL()
 if err != nil {
  panic(err)
 }

 fmt.Printf("SQL: %s, Args: %v\n", sql, args)
 // SQL: DELETE `users` FROM `users` WHERE (`id` = 1), Args: []
}

より実践的な使い方

goquでの基本的なCRUD操作の紹介が終わったところで、次はより実践的な機能を紹介します。

Expressionsを使ってクエリを組み立てる

Expressionsは、Where、From、Selectなどの部分のより詳細にな記述をサポートします。すべては紹介しきれないので、よく使われそうなものをピックアップして紹介します。

Ex{}

Ex{}はWhere句で最も使われるであろう型で、columnとvalueを等号演算子で比較する際にまとめて記述するために使われます。

ds := dialect.From("users").
    Where(
        goqu.Ex{
            "col1": 1,
            "col2": "val2",
            "col3": true,
            "col4": nil,
            "col5": []string{"val1", "val2"},
        },
    )
sql, args, err := ds.ToSQL()
if err != nil {
    panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT * FROM `users` WHERE ((`col1` = 1) AND (`col2` = 'val2') AND (`col3` IS TRUE) AND (`col4` IS NULL) AND (`col5` IN ('val1', 'val2'))), Args: []

ExOr{}

ExOr{}Ex{}と似ていますが、各条件がAndではなくOrで判定されます。

ds := dialect.From("users").
    Where(
        goqu.ExOr{
            "col1": 1,
            "col2": "val2",
        },
    )
sql, args, err := ds.ToSQL()
if err != nil {
    panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT * FROM `users` WHERE ((`col1` = 1) OR (`col2` = 'val2')), Args: []

C()

C()はcolumnを表す識別子で、Where()などと一緒に使われます。

ds := dialect.From("users").
    Where(goqu.C("col1").Eq(1))
sql, args, err := ds.ToSQL()
if err != nil {
    panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT * FROM `users` WHERE (`col1` = 1), Args: []

V()

V()はクエリで直接値を使いたい場合に使われます。

ds := dialect.From("users").
    Select(
        goqu.V(true).As("is_valid"),
        goqu.V(0.5).As("tax_rate"),
    ).
    Where(goqu.C("col1").Eq(1))
sql, args, err := ds.ToSQL()
if err != nil {
    panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT 1 AS `is_valid`, 0.5 AS `tax_rate` FROM `users` WHERE (`col1` = 1), Args: []

And()

And()Where()の中で複数の条件を定義する際に使われます。

ds := dialect.From("users").
    Where(
        goqu.And(
            goqu.C("col1").Eq(1),
            goqu.C("col2").Eq(2),
        ),
    )
sql, args, err := ds.ToSQL()
if err != nil {
    panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT * FROM `users` WHERE ((`col1` = 1) AND (`col2` = 2)), Args: []

Or()

Or()And()と同様にWhere()の中で使われます。

ds := dialect.From("users").
    Where(
        goqu.Or(
            goqu.C("col1").Eq(1),
            goqu.C("col2").Eq(2),
        ),
    )
sql, args, err := ds.ToSQL()
if err != nil {
    panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT * FROM `users` WHERE ((`col1` = 1) OR (`col2` = 2)), Args: []

Prepared Statementsを使う

デフォルトでは、goquはすべてのパラメータを直接クエリに埋め込みますが、パラメータを分けたい場合は、Prepared()を使うことでこれを実現します。

ds := dialect.From("users").
    Where(
        goqu.Ex{
            "col1": 1,
            "col2": "b",
            "col3": true,
            "col4": []int{1, 2, 3},
        },
    )
sql, args, err := ds.Prepared(true).ToSQL()
if err != nil {
    panic(err)
}

fmt.Printf("SQL: %s, Args: %v\n", sql, args)
// SQL: SELECT * FROM `users` WHERE ((`col1` = ?) AND (`col2` = ?) AND (`col3` IS TRUE) AND (`col4` IN (?, ?, ?))), Args: [1 b 1 2 3]

Scanした結果を構造体に反映する

ScanStructs()を使うことで、Select句で指定したcolumnを構造体に反映することができます。

注意点としては、事前にsql.Open()でコネクションを張っておき、それをDiralectWrapperのDB()メソッドで渡しておく必要があります。

package main

import (
 "database/sql"
 "fmt"

 "github.com/doug-martin/goqu/v9"
 // Import the mysql dialect
 _ "github.com/doug-martin/goqu/v9/dialect/mysql"
)

func main() {
 dialect := goqu.Dialect("mysql")

 mysqlDB, err := sql.Open("mysql", "root:root@(127.0.0.1:3306)/test")
 if err != nil {
  panic(err)
 }

 dialect.DB(mysqlDB)

 type user struct {
  FirstName string `db:"first_name"`
  LastName  string `db:"last_name"`
  Age       int    `db:"-"` // Ignored
 }
 var users []user
 if err := dialect.From("users").
  Select(&user{}).
  ScanStructs(&users); err != nil {
  panic(err)
 }
 fmt.Printf("Users: %+v\n", users)
 // Users: [{FirstName:John LastName:Doe Age:0}]
}

さいごに

ここでは主にgoquの基本的な使い方を紹介しましたが、まだまだたくさんの便利な機能がありますので、goqu良さそうだなと思っていただけた方は、ぜひドキュメントをご覧ください。

おすすめ書籍

Hiroki Ono

シェア
執筆者:
Hiroki Ono
タグ: golang

最近の投稿