SQLBoiler の使い方を簡単にまとめた

2020-03-01

最近、業務で使っている SQLBoiler の使い方についてわかったことをまとめておきます。

目次

SQLBoilerとは?

実際のデータベーススキーマから Go の ORMコード を自動生成してくれるライブラリです。

概要についてはあまり話さないので他の記事をご覧ください。

リポジトリ: volatiletech/sqlboiler

自動生成するためのコマンドは以下のようにインストールします。

$ go get -u -t github.com/volatiletech/sqlboiler

SQLBoiler が対象とするテーブルは一つ以上の主キーをもつ必要があります。ただしサロゲートキーである必要はありません。 (ないと Error: unable to initialize tables: primary key missing in tables (pilots) のようになります)

後述しますが ユニークキー, 外部キー, NOT NULL のような制約も自動生成されるコードに影響するので 用途に合わせてできるだけ設定しておくとよいでしょう。

接続情報 connection

以下のように接続情報を設定ファイルに書きます。形式は TOML, YAML, JSON のいずれかです。 今回は TOML にしました。

sqlboiler-example/sqlboiler.toml
[psql]
  dbname = "db"
  host   = "postgres"
  port   = 5432
  user   = "usr"
  pass   = "pw"
  sslmode = "disable"
  schema = "public"
  blacklist = ["migrations", "other"]

[mysql]
  dbname  = "db"
  host    = "mysql"
  port    = 3306
  user    = "usr"
  pass    = "pw"
  sslmode = "false"

RDBMSの違いによる実装方法はさほど違いありませんが、 設定や出力方法、接続ついては多少の違いがあります。

MySQL

PostgreSQL

# スキーマ作成
$ mysql -h mysql -uusr -p db < schema.sql
Enter password: # pw

$ go get github.com/volatiletech/sqlboiler/drivers/sqlboiler-mysql
$ sqlboiler mysql --output models_mysql --pkgname models_mysql --wipe
# スキーマ作成
$ psql -h postgres -U usr db -f schema.sql
Password for user usr: # pw

$ go get github.com/volatiletech/sqlboiler/drivers/sqlboiler-psql
$ sqlboiler psql --output models_psql --pkgname models_psql --wipe

sqlboiler-example/mysql.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")
	jets, _ := models.Jets().All(context.Background(), db)
	fmt.Println("jets:", jets)
}

sqlboiler-example/psql.go
package main
  
import (
	"context"
	"database/sql"
	"fmt"

	models "sqlboiler-example/models_psql"

	_ "github.com/lib/pq"
)

func main() {
	db, _ := sql.Open(
		"postgres",
		"host=postgres dbname=db user=usr password=pw sslmode=disable")
	jets, _ := models.Jets().All(context.Background(), db)
	fmt.Println("jets:", jets)
}

今回は複数のDBがあるため便宜的に出力先およびパッケージ名を指定しましたが 指定しなければデフォルトで models になります。

--output

出力先のディレクトリを変更する(default: models)

--pkgname

パッケージ名を変更する(default: models)

https://github.com/volatiletech/sqlboiler#configuration

スキーマ作成時に指定するセッションと利用時に接続するセッションは異なるので混同しないようにしましょう。

備考

上記の例では 一度しかDBに接続しなかったため、とくに不便さを感じませんでしたが DBに接続するたびに db セッションを指定するのはめんどくさいと感じるかもしれません。

boil.SetDB(db) とすることで接続情報をグローバル領域に保存してそれを利用することができます。

この接続情報は .AllG() のように G を末尾につけた関数を呼び出すことで利用できます。

P を指定すると エラーのときにパニックが発生するようになります。(err の返却値がなくなるため注意)

G と併用する場合 G を先に書き、 AllGP() のようにします。

以降のセクションでは MySQL を前提に進めていきます。

Model

SQLBoiler では 一つのテーブルをモデル構造体として出力します。

1レコードを扱うためのメソッドが以下のように定義されています。

Insert

レコードをテーブルに追加するためのメソッドです。

sqlboiler-example/insert.go
package main

import (
	"context"
	"database/sql"

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/boil"
)

func main() {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")
	pilot := models.Pilot{
		ID:   1,
		Name: "Righ",
	}
	pilot.Insert(context.Background(), db, boil.Infer())
}
root@8ebb694c0f41:~# go run insert.go
root@8ebb694c0f41:~# mysql -h mysql -uusr -ppw db -e "SELECT * FROM pilots;"
+----+------+
| id | name |
+----+------+
|  1 | Righ |
+----+------+

備考

バルクインサートはサポートされていません。

SQLBoilerでBulk Insertを実現する方法 - Qiita のようにカスタムテンプレートを書けば実現できるようです。

Update

変更後のレコードをテーブルに反映させるメソッドです。

sqlboiler-example/update.go
package main

import (
	"context"
	"database/sql"

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/boil"
)

func main() {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")
	pilot := models.Pilot{
		ID:   1,
		Name: "Crohaco",
	}
	pilot.Update(context.Background(), db, boil.Infer())
}
root@8ebb694c0f41:~# mysql -h mysql -uusr -ppw db -e "SELECT * FROM pilots;"
+----+---------+
| id | name    |
+----+---------+
|  1 | Crohaco |
+----+---------+
Reload

レコードを最新の状態に更新するメソッドです。

sqlboiler-example/reload.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")
	pilot := models.Pilot{
		ID:   1,
		Name: "",
	}
	pilot.Reload(context.Background(), db)
	fmt.Println(pilot.ID, pilot.Name)
}
root@8ebb694c0f41:~# go run reload.go
1 Crohaco
Upsert

レコードが既に登録されていれば更新、されていなければ追加するメソッドです。 登録の有無はプライマリキーによって行われます。(要検証)

3番目引数には UPDATEフィールド, 4番目の引数には INSERTフィールド を指定します。

sqlboiler-example/upsert.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/boil"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")
	pilot := models.Pilot{
		ID:   1,
		Name: "Righ",
	}
	if err := pilot.Upsert(ctx, db, boil.Infer(), boil.Infer()) {
		fmt.Println(err)
	}

	jet1 := models.Jet{
		ID:      1,
		PilotID: pilot.ID,
		Age:     10,
		Name:    "F-15",
		Color:   "Gray",
	}
	if err := jet1.Upsert(ctx, db, boil.Infer(), boil.Infer()) {
		fmt.Println(err)
	}
	
	jet1.Age = 0
	jet1.Name = "MRJ"
	jet1.Color = "White"
	if err := jet1.Upsert(ctx, db, boil.Whitelist("name", "color"), boil.Infer()); err != nil {
		fmt.Println(err)
	}

	jet2 := models.Jet{
		ID:      2,
		PilotID: pilot.ID,
		Age:     20,
		Name:    "F-14",
		Color:   "Gray",
	}
	if err := jet2.Upsert(ctx, db, boil.Whitelist("name", "color"), boil.Infer()); err != nil {
		fmt.Println(err)
	}
}
root@8ebb694c0f41:~# go run upsert.go
root@8ebb694c0f41:~# mysql -h mysql -uusr -ppw db -e "SELECT * FROM jets;"
+----+----------+-----+------+-------+
| id | pilot_id | age | name | color |
+----+----------+-----+------+-------+
|  1 |        1 |  10 | MRJ  | White |
|  2 |        1 |  20 | F-14 | Gray  |
+----+----------+-----+------+-------+

フィールドは boil.Whitelist("field", "field2") のように可変長で複数指定します。 id=1 のレコードは age が更新されていないのがわかりますね。

警告

UPDATEのフィールド以外に追加のフィールドも指定できますが、 追加するフィールドを限定する場合はスキーマにデフォルト値が設定されている必要があります。

(MySQLの場合)例えば boil.Whitelist("id", "pilot_id") を INSERTフィールド に指定すると以下のようなエラーになります。

models_mysql: unable to upsert for jets: Error 1364: Field 'age' doesn't have a default value

フィールドを指定しない場合は boil.Infer() を指定すると推測されたフィールドが指定されます。

Delete

該当レコードをテーブルから削除するためのメソッドです。

sqlboiler-example/delete.go
package main

import (
	"context"
	"database/sql"

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")
	ctx := context.Background()
	pilot, _ := models.FindPilot(ctx, db, 1)
	pilot.Delete(ctx, db)
}
root@8ebb694c0f41:~# go run delete.go
root@8ebb694c0f41:~# mysql -h mysql -uusr -ppw db -e "SELECT * FROM pilots;"
root@8ebb694c0f41:~#

備考

  • テーブルが外部から参照されている場合、 外部レコードを作るためのメソッド(Add{モデル}()s?, Set{モデル}s?)が作られますが 制約によって作られるメソッドは異なります。

    • 例えば Pilot モデルは AddJets メソッドを持ちますが、 jets.pilot_id に UNIQUE制約があったり、 pilot_id 自体を主キーに設定した場合、 AddJets ではなく SetJet メソッドが作られるという具合です。

  • (記憶が曖昧ですが) 主キーを間違えた場合に AddJet (単数形)が作られたこともありました。 期待通りのモデルが作られない場合はスキーマを見直してみてください。

  • 2カラムしかない純粋な中間テーブルはモデル自体が生成されないと言った挙動をするみたいです。

  • NOT NULL 制約がないとモデルのフィールドはプリミティブな型ではなく null.String のような型で出力されるようです。

    • 伝聞ですが扱いづらいということなので できる限り NOT NULL 制約をつけるようにしたほうがよいでしょう。

Query

テーブルからレコードを抽出するためのメソッドです。

Django をやったことがあるなら QuerySet と同じようなものと考えるとわかりやすいです。

さて、どのように呼び出すかというと モデルを複数形にしたメソッド名をコールします。 models.Jet モデルなら models.Jets(mods...) のようにします。

Mod

SQLBoilerはクエリに対する操作を mod という概念で表現し Queryメソッドに指定できます。

Select

抽出対象の列を指定

InnerJoin

内部結合のテーブルと条件を指定

Where

検索条件を指定

Eq

完全一致検索の条件を指定

GroupBy

グルーピングする列を1つ以上指定する

OrderBy

ソート対象の列を1つ以上指定する

Having

HAVING(グルーピング後の絞り込み条件)を指定する

Limit

LIMITを指定する

Offset

OFFSETを指定する

Load

Earger load する列を指定する

SELECT pilots.name, MIN(jets.age) AS age
FROM jets
INNER JOIN pilots ON pilots.id = jets.pilot_id
WHERE jets.age >= 20
GROUP BY pilots.name
HAVING COUNT(*) >= 2
ORDER BY age desc
LIMIT 2
OFFSET 1
;

のようなSQLを作ろうとしたら以下のように書きます。

sqlboiler-example/mods.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/queries/qm"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	jets, err := models.Jets(
		qm.Select("pilots.name as name", "MIN(jets.age) AS age"),
		qm.InnerJoin("pilots on pilots.id = jets.pilot_id"),
		qm.Where("age >= 20"),
		qm.GroupBy("pilots.name"),
		qm.Having("count(*) >= 2"),
		qm.OrderBy("age DESC"),
		qm.Limit(2),
		qm.Offset(1),
	).All(ctx, db)
	if err != nil {
		fmt.Println(err)
	}
	for _, jet := range jets {
		fmt.Println(jet.Name, jet.Age)
	}
}
root@8ebb694c0f41:~# go run mods.go
Albert 20
Warner 20

Eager loading

先程軽く触れましたが、少し詳細に触れておきます。

qm.Load("Field") とすることで 外部キーのIDをまとめて参照先テーブルに別クエリを発行し、取得した結果を自動的に紐付けてくれます。 SQL自体を変更するものではないと言う意味で ほかの Mod とは少々異なる機能と言えます。

Django でいうなら prefetch_related です。イメージがわかない方はこちらを見たほうがわかりやすいかもしれません。

外部キーのリレーションを一つずつ解決しても同じ結果を得ることはできますが、 レコード件数の回数クエリが発生する所謂 N+1問題 となるためおすすめできません。

Load の第一引数はリレーションを表す ドット区切りの文字列です。 この文字列は XXXRels (XXXはモデル名)に定義されているのでそれを使うのが確実です。

(ドット区切りにする場合は自分で組み立てる必要がある)

sqlboiler-example/load.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/queries/qm"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	pilots, err := models.Pilots(
		qm.Load("Languages"),
	).All(ctx, db)
	if err != nil {
		fmt.Println("err:", err)
	}
	fmt.Println("num of pilots:", len(pilots))
	for _, pilot := range pilots {
		fmt.Printf("%s: ", pilot.Name)
		for _, lang := range pilot.R.Languages {
			fmt.Printf("%s ", lang.Language)
		}
		fmt.Println()
	}
}

ちなみにLoadしたリレーションは R 経由で参照できます。 リレーションが単数形か複数系かはリレーションの向きとユニーク制約によります。

languagespilots は中間テーブルにより N:M の関係になっているため複数形での参照となります。

備考

別のリレーションを参照するときに真っ先に思い浮かぶのは JOIN ですね。

では JOIN と比べた Eager load の優位性はなんでしょうか?

JOIN は結合条件に該当するレコードが複数あれば行数が増え、該当がなければ行数は0になります。 この性質はうまく使えばありがたいものですが、単に一覧したい場合は扱いにくいものでしょう。

Eager load で取得したリレーションはその件数によらず、もとのレコード数に影響を与えません。

先程のコードを見てもわかるかもしれませんが、 qm.InnerJoin によって結合したフィールドは Select() で指定することで参照できますが、それを受けるためのフィールドが定義された構造体が必要です。

https://github.com/volatiletech/sqlboiler/issues/536

おそらく、 SQLBoiler は外部リレーション取得は基本的に JOIN ではなく Eager load で行うという コンセプトなんでしょう。(LEFT JOIN も使えないしね)

警告

qm.Select でフィールドを限定する場合、外部キーのフィールドも含めないと ロードしたリレーションが nil になるので注意しましょう。

Finisher

クエリはそのままでは利用できません。

Finisherを使ってクエリから結果を抽出したり、テーブルに変更を加えるための操作が Finisher です。

One

該当レコードの先頭をモデル(構造体)のリファレンスで取得します。

sqlboiler-example/one.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	pilot, err := models.Pilots().One(ctx, db)
	if err != nil {
		fmt.Println("err:", err)
	}
	fmt.Println(pilot)
}
root@8ebb694c0f41:~# go run one.go
&{1 Manfred <nil> {}}
All

すべての該当レコードをモデルのスライスで取得します。

sqlboiler-example/all.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	pilots, err := models.Pilots().All(ctx, db)
	if err != nil {
		fmt.Println("err:", err)
	}
	for _, pilot := range pilots {
		fmt.Println(pilot.Name)
	}
}
root@8ebb694c0f41:~# go run all.go
Manfred
William
Albert
Warner
Andrew
Count

該当レコードの数を返却します。

sqlboiler-example/count.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	count, err := models.Pilots().Count(ctx, db)
	if err != nil {
		fmt.Println("err:", err)
	}
	fmt.Println("num of pilots:", count)
}
root@8ebb694c0f41:~# go run count.go
num of pilots: 5
UpdateAll

該当レコードをすべて更新します。更新内容は map で指定します。

sqlboiler-example/update_all.go
package main

import (
	"context"
	"database/sql"

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	models.Pilots().UpdateAll(ctx, db, map[string]interface{}{
		"name": "Saki",
	})
}
root@8ebb694c0f41:~# mysql -h mysql -uusr -ppw db -e "SELECT * FROM pilots;"
+----+------+
| id | name |
+----+------+
|  1 | Saki |
|  2 | Saki |
|  3 | Saki |
|  4 | Saki |
|  5 | Saki |
+----+------+
DeleteAll

該当レコードをすべて削除します。

sqlboiler-example/delete_all.go
package main

import (
	"context"
	"database/sql"

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	models.Jets().DeleteAll(ctx, db)
}
root@8ebb694c0f41:~# go run delete_all.go
root@8ebb694c0f41:~# mysql -h mysql -uusr -ppw db -e "SELECT * FROM jets;"
root@8ebb694c0f41:~#
Exists

該当するレコードが1件以上存在することを確認します。

sqlboiler-example/exists2.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	exists, err := models.Pilots().Exists(ctx, db)
	if err != nil {
		fmt.Println("err:", err)
	}
	fmt.Println(exists)
}
root@8ebb694c0f41:~# go run exists2.go
true

Shortcuts

クエリを経由して Finisher で結果を得られますが、単純な処理を長々と書きたくないものです。

以下のメソッドについては Queryを介さずに呼び出すことができます。

Find

Query(qm.Where("id=?", id)).One(ctx, db) と同じです。

sqlboiler-example/exists.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	exists1, _ := models.PilotExists(ctx, db, 1)
	exists2, _ := models.PilotExists(ctx, db, 2)

	fmt.Println(exists1, exists2)
}
root@8ebb694c0f41:~# go run find.go
&{1 Crohaco <nil> {}} <nil>
Exists

Query(qm.Where("id=?", id)).Exists(ctx, db) と同じです。

sqlboiler-example/exists.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	exists1, _ := models.PilotExists(ctx, db, 1)
	exists2, _ := models.PilotExists(ctx, db, 2)

	fmt.Println(exists1, exists2)
}
root@8ebb694c0f41:~# go run exists.go
true false

Raw query

これまでの機能ではサブクエリ を含む複雑なクエリや LEFT JOIN などを実現できません。

一応 Raw() によって生SQLを書けばカバーできるのでその方法を抑えておきましょう。

Bind

Raw によって生成されたクエリを Bind() によって構造体に紐付けることでレコードを抽出できます。

このときフィールド名を構造体のフィールド合わせる必要があります。 複数の構造体を埋め込んでフィールド名が重複している場合 {モデル名(小文字).フィールド名} とします。 (READMEだと複数形で書いてあったけど動かなかった..)

sqlboiler-example/raw.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/queries"
)

type PilotAndJet struct {
	models.Pilot    `boil:",bind"`
	models.Language `boil:",bind"`
}

func main() {
	var pajs []PilotAndJet
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")
	if err := queries.Raw(`
		SELECT
		  p.id AS "pilot.id", p.name, COALESCE(l.language, 'NO LANG') AS language
		FROM pilots p
		  LEFT JOIN pilot_languages pl ON pl.pilot_id = p.id
		  LEFT JOIN languages l ON l.id = pl.language_id AND p.id != 1`,
	).Bind(context.Background(), db, &pajs); err != nil {
		fmt.Println(err)
	}
	for _, paj := range pajs {
		fmt.Println(paj)
	}
}
$ go run raw.go
{{1 Saki <nil> {}} {0 NO LANG <nil> {}}}
{{2 Saki <nil> {}} {0 English <nil> {}}}
{{3 Saki <nil> {}} {0 English <nil> {}}}
{{4 Saki <nil> {}} {0 German <nil> {}}}
{{4 Saki <nil> {}} {0 English <nil> {}}}
{{5 Saki <nil> {}} {0 NO LANG <nil> {}}}

LEFT JOIN で NULL になると変換に失敗するので COALESCE 関数などで補完する必要がありそうです。 failed to bind pointers to obj: sql: Scan error on column index 2, name "language": converting NULL to string is unsupported

Exec

Exec は クエリを実行するためのメソッドです。 SELECT 以外の変更を加える文を実行するために使います。

sqlboiler-example/exec.go
package main

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

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/queries"
)

func main() {
	ctx := context.Background()
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	queries.Raw(`
		INSERT INTO pilots (id, name)
		VALUES (6, 'Diddy')
	`).Exec(db)

	pilot, _ := models.FindPilot(ctx, db, 6)
	fmt.Println(pilot)
}
root@8ebb694c0f41:~# go run exec.go
&{6 Diddy <nil> {}}

おまけ

以下のメソッドはクエリの実行結果を sql.Row または sql.Rows で返します。

SQLBoilerをメインで使っていればあまり使うことはないかもしれませんが、 これまでの資産としてこれらを引数にしたユーティリティーなどを持っていれば使ってみましょう。

QueryRow

sqlboiler-example/queryrow.go
package main

import (
	"database/sql"
	"fmt"

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/queries/qm"
)

func main() {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	row := models.Pilots(
		qm.Select("id", "name"),
	).Query.QueryRow(db)

	var id int
	var name string
	if err := row.Scan(&id, &name); err != nil {
		fmt.Println("err:", err)
	}
	fmt.Println(id, name)
}
root@8ebb694c0f41:~# go run queryrow.go
1 Saki
Query

sqlboiler-example/query.go
package main

import (
	"database/sql"
	"fmt"

	models "sqlboiler-example/models_mysql"

	_ "github.com/go-sql-driver/mysql"
	"github.com/volatiletech/sqlboiler/queries/qm"
)

func main() {
	db, _ := sql.Open("mysql", "usr:pw@tcp(mysql)/db")

	rows, err := models.Pilots(
		qm.Select("id", "name"),
	).Query.Query(db)
	if err != nil {
		fmt.Println("err:", err)
	}
	defer rows.Close()

	for rows.Next() {
		var id int
		var name string
		if err := rows.Scan(&id, &name); err != nil {
			fmt.Println("err:", err)
			continue
		}
		fmt.Println(id, name)
	}
}
root@8ebb694c0f41:~# go run query.go
1 Saki
2 Saki
3 Saki
4 Saki
5 Saki

終わりに Conclusion

現状ではまだ発展途上なのですこし複雑なクエリを書こうとするとどうしても生SQLに頼るしかなさそうですが、 DBスキーマからコードを生成するというコンセプトはおもしろいものだと思います。

スキーマファーストの開発では採用してみても面白いかもしれません。

自分は ORMに引きづられたくないので sqlx とか薄いORMが好みです。