このエントリーをはてなブックマークに追加

はじめに

でセットアップ完了と更新系ができしたので、続いてselect系について書いていこうと思います。
SQLBoilerのselect系はかなりのことを行うことができます。

Query Mod

SQLBoilerを使うにあたってはQuery Modを理解することが大事です。
Query ModはSQLBoiler独自の概念で、SQLの条件をtype safeに定義できる仕組みです。
Query Modは qm パッケージに属しています。

例えば、where区を書きたい場合は qm.Where("user.id = ?", userID) などのように定義できます。
そして、Query Modで条件を定義した上で最後にFinisherを使います。
FinisherがSQLの基本的な形を決定します。
Finisherにはいくつかの種類がありますが、よく使うのは All (select *) One (select * … limit 1) Count (select count(*)) あたりかと思います。
全容については以下をご参照ください。
https://github.com/volatiletech/sqlboiler#finishers

実際にSQLを発行してみますが、いずれも↓のDDLで作ったテーブルを使っています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
CREATE TABLE groups
(
	id serial NOT NULL,
	name text NOT NULL,
	description text NOT NULL,
	created_at timestamp NOT NULL,
	updated_at timestamp NOT NULL,
	PRIMARY KEY (id)
) WITHOUT OIDS;


CREATE TABLE group_members
(
	id bigserial NOT NULL,
	user_id int NOT NULL,
	group_id int NOT NULL,
	role text NOT NULL,
	created_at timestamp NOT NULL,
	updated_at timestamp NOT NULL,
	PRIMARY KEY (id),
	CONSTRAINT UQ_group_members_user_group UNIQUE (user_id, group_id)
) WITHOUT OIDS;

CREATE TABLE users
(
	id serial NOT NULL,
	email text UNIQUE,
	password_digest text,
	created_at timestamp NOT NULL,
	updated_at timestamp NOT NULL,
	PRIMARY KEY (id)
) WITHOUT OIDS;

ALTER TABLE group_members
	ADD CONSTRAINT FK_group_members_groups FOREIGN KEY (group_id)
	REFERENCES groups (id)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;

ALTER TABLE group_members
	ADD CONSTRAINT FK_group_members_users FOREIGN KEY (user_id)
	REFERENCES users (id)
	ON UPDATE RESTRICT
	ON DELETE RESTRICT
;

単一テーブル

テーブル全件取得

まずは簡単な全件取得です。ユーザを全件取得してい見ます。

1
2
users := db.Users().AllGP(context.Background())
fmt.Printf("users = %+v\n", users)
1
2
3
4
$ go run main.go
SELECT * FROM "users";
[]
users = [0xc000164100]

全件なので、Allを使っています。sliceで返ってきていることがわかりますね。
末尾のGとPはそれぞれ以下の意味となります。

  • G ・・・ GlobalなDBコネクションを使う。トランザクション制御をしたい場合、Beginした時の sql.Tx を使います。
  • P ・・・ エラーが発生したら err を返すのではなく、panicを起こす。productionで使う場合は基本的にPは使わないほうがよいです。

1件取得

続いて1件取得してみます。

1
2
user := db.Users().OneGP(context.Background())
fmt.Printf("user = %+v\n", user)
1
2
3
4
SELECT * FROM "users" LIMIT 1;
[]
user = &{ID:1 Email:{String:test@example.com Valid:true} PasswordDigest:{String:digested-password Valid:true} CreatedAt:2019-03-25 13:52:29.148263 +0000 +0000 UpdatedAt:2019-03-25 13:52:29.
148263 +0000 +0000 R:<nil> L:{}}

Allとの違いとしては、 One を指定するとSQLに limit 1 が追加されています。
また、 All のときはsliceで返ってきていたのがOneだとstructで返ってきました。

Join

続いてjoinしてみます。memberに所属しているuserだけを取得します。

1
2
users := db.Users(qm.InnerJoin("group_members on group_members.user_id = users.id")).AllGP(context.Background())
fmt.Printf("users = %+v\n", users)
1
2
3
4
$ go run main.go
SELECT "users".* FROM "users" INNER JOIN group_members on group_members.user_id = users.id;
[]
users = [0xc000184100]

ちゃんとJoinできました。

joinしたデータを取得

joinしたデータを取得するのは少し面倒です。取得したいentityを含んだstructを作る必要があります。

1
2
3
4
5
6
7
type userMember struct {
	db.User        `boil:",bind"`
	db.GroupMember `boil:",bind"`
}
var mem userMember
db.Users(qm.Select("users.*, group_members.*"), qm.InnerJoin("group_members on group_members.user_id = users.id")).BindG(context.Background(), &mem)
fmt.Printf("mem = %+v\n", mem)
1
2
3
4
5
$ go run main.go
SELECT users.*, group_members.* FROM "users" INNER JOIN group_members on group_members.user_
id = users.id;
[]
mem = {User:{ID:1 Email:{String:test@example.com Valid:true} PasswordDigest:{String:digested-password Valid:true} CreatedAt:2019-03-28 23:41:47.305423 +0000 +0000 UpdatedAt:2019-03-2823:41:47.305423 +0000 +0000 R:<nil> L:{}} GroupMember:{ID:2 UserID:1 GroupID:1 Role:admin CreatedAt:2019-03-25 13:52:29.148263 +0000 +0000 UpdatedAt:0001-01-01 00:00:00 +0000 UTC R:<nil> L:{}}}

joinして一括でデータをガツッと取得したい場合なんかは良さそうです。

Eager Loading

ユーザが所属するメンバーとグループをEagerLoadingで取得してみます。
QueryModに Load という関数があるので、これを利用します。

1
2
3
4
5
6
7
user := db.Users(qm.Load("GroupMembers.Group")).OneGP(context.Background())
fmt.Printf("user = %+v\n", user)
fmt.Printf("user.R.GroupMembers = %+v\n", user.R.GroupMembers)
for _, mem := range user.R.GroupMembers {
	fmt.Printf("mem = %+v\n", mem)
	fmt.Printf("mem.R.Group = %+v\n", mem.R.Group)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ go run main.go
SELECT * FROM "users" LIMIT 1;
[]
SELECT * FROM "group_members" WHERE ("user_id" IN ($1));
[1]
SELECT * FROM "groups" WHERE ("id" IN ($1));
[1]
user = &{ID:1 Email:{String:test@example.com Valid:true} PasswordDigest:{String:digested-password Valid:true} CreatedAt:2019-03-25 13:52:29.148263 +0000 +0000 UpdatedAt:2019-03-25 13:52:29.148263 +0000 +0000 R:0xc00008c800 L:{}}
user.R.GroupMembers = [0xc00016c540]
mem = &{ID:2 UserID:1 GroupID:1 Role:admin CreatedAt:2019-03-28 23:41:47.305423 +0000 +0000 UpdatedAt:2019-03-28 23:41:47.305423 +0000 +0000 R:0xc000082520 L:{}}
mem.R.Group = &{ID:1 Name:test Description:test CreatedAt:2019-03-28 23:41:10.756431 +0000 +0000 UpdatedAt:2019-03-28 23:41:10.756431 +0000 +0000 R:0xc00008ca60 L:{}}

これでuserに紐づくgroup_memberとgroupを一括で取得しに行くことができました。
データの取得は inner join ではなく、個々のSQLがそれぞれ発行されていることがわかります。
これでN+1 Queryを撲滅しましょう。

特定の条件をつける

Loadingする際に、特性の条件のデータを取得することもできます。
今回はユーザに紐づくadmin権限を持ったメンバーとグループを取得してみます。

1
2
3
4
5
6
7
user := db.Users(qm.Load("GroupMembers", qm.Where("group_members.role = ?", "admin")), qm.Load("GroupMembers.Group")).OneGP(context.Background())
fmt.Printf("user = %+v\n", user)
fmt.Printf("user.R.GroupMembers = %+v\n", user.R.GroupMembers)
for _, mem := range user.R.GroupMembers {
	fmt.Printf("mem = %+v\n", mem)
	fmt.Printf("mem.R.Group = %+v\n", mem.R.Group)
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$ go run main.go
SELECT * FROM "users" LIMIT 1;
[]
SELECT * FROM "group_members" WHERE ("user_id" IN ($1)) AND (group_members.role = $2);
[1 admin]
SELECT * FROM "groups" WHERE ("id" IN ($1));
[1]
user = &{ID:1 Email:{String:test@example.com Valid:true} PasswordDigest:{String:digested-password Valid:true} CreatedAt:2019-03-25 13:52:29.148263 +0000 +0000 UpdatedAt:2019-03-25 13:52:29.148263 +0000 +0000 R:0xc00000eaa0 L:{}}
user.R.GroupMembers = [0xc00012ca80]
mem = &{ID:2 UserID:1 GroupID:1 Role:admin CreatedAt:2019-03-28 23:41:47.305423 +0000 +0000 UpdatedAt:2019-03-28 23:41:47.305423 +0000 +0000 R:0xc000013030 L:{}}
mem.R.Group = &{ID:1 Name:test Description:test CreatedAt:2019-03-28 23:41:10.756431 +0000 +0000 UpdatedAt:2019-03-28 23:41:10.756431 +0000 +0000 R:0xc00000ed80 L:{}}

2つ目のgroup_membersのselect SQLの条件にadminであることという条件が入っていることがわかります。
今回はgroup_membersが取得できたので、その先のgroupsまでselectしにいっています。
例えば、2つ目のSQLで結果が取れない場合、3つ目のgroupsのSQLは発行されないことが期待されます。
実際に試してみましょう。変更点は admin を条件にしている部分を dummy に変えて見ます。
以下は実行結果です。

1
2
3
4
5
6
7
$ go run main.go
SELECT * FROM "users" LIMIT 1;
[]
SELECT * FROM "group_members" WHERE ("user_id" IN ($1)) AND (group_members.role = $2);
[1 dummy]
user = &{ID:1 Email:{String:test@example.com Valid:true} PasswordDigest:{String:digested-password Valid:true} CreatedAt:2019-03-25 13:52:29.148263 +0000 +0000 UpdatedAt:2019-03-25 13:52:29.148263 +0000 +0000 R:0xc000154200 L:{}}
user.R.GroupMembers = []

結果は期待通りでした。group_membersが0件になっているので、groupsを取得するSQLは発行されていません。

まとめ

今回はSQLBoilerのselect系のSQLの発行方法についてまとめてみました。
基本的には全て SQLBoilerのREADME にやり方は書いてあります。
ここで書いてあるとおり、SQLBoilerでは多くのSQLの形態がサポートされているので、やりたいことは大方できるかと思います。
今回は紹介していませんが、その他にも集約関数の group by も利用できます。
SQLBoiler便利なので是非是非使っていきましょう。