Golang の ORM、GORMを使用していてちょっとした値の取得にえらく手間取ったのでメモ。
ちなみに使い方を知らないだけでもっと良いやり方があるかもしれない。
(というか、もっと良いやり方のツッコミ待ちのため記事を書いた)
テーブル定義
このようなテーブルがあるとする。
MariaDB [zaneli]> desc foo;
+-------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| id | int(11) | YES | | NULL | |
| name | varchar(200) | YES | | NULL | |
+-------+--------------+------+-----+---------+-------+
2 rows in set (0.01 sec)
MariaDB [zaneli]> desc foobar;
+--------+---------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+---------+------+-----+---------+-------+
| foo_id | int(11) | YES | | NULL | |
| bar_id | int(11) | YES | | NULL | |
+--------+---------+------+-----+---------+-------+
2 rows in set (0.00 sec)
foo
と foo_bar
は 1:N で紐付いているとする。
MariaDB [zaneli]> select * from foo;
+------+------+
| id | name |
+------+------+
| 1 | foo1 |
| 2 | foo2 |
| 3 | foo3 |
+------+------+
3 rows in set (0.00 sec)
MariaDB [zaneli]> select * from foobar;
+--------+--------+
| foo_id | bar_id |
+--------+--------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 2 | 1 |
+--------+--------+
4 rows in set (0.00 sec)
MariaDB [zaneli]> select * from foo f left outer join foobar fb on f.id = fb.foo_id;
+------+------+--------+--------+
| id | name | foo_id | bar_id |
+------+------+--------+--------+
| 1 | foo1 | 1 | 1 |
| 1 | foo1 | 1 | 2 |
| 1 | foo1 | 1 | 3 |
| 2 | foo2 | 2 | 1 |
| 3 | foo3 | NULL | NULL |
+------+------+--------+--------+
5 rows in set (0.00 sec)
テーブルに対応する構造体を定義
type Foo struct {
ID int
Name string
BarIDs []int
// `Related` `Preload` を使う際には `FooBars` を持たせる
// FooBars []FooBar
}
func (Foo) TableName() string { return "foo" }
type FooBar struct {
FooID int
BarID int
}
func (FooBar) TableName() string { return "foobar" }
GORM で値を取得
まずは foo だけ取得してみる。
import (
"fmt"
"log"
"github.com/jinzhu/gorm"
_ "github.com/jinzhu/gorm/dialects/mysql"
)
func getFoo(db *gorm.DB) {
foo := Foo{ID: 1}
if err := db.Find(&foo).Error; err != nil {
log.Fatal(err)
}
fmt.Println(foo)
}
発行したSQLと結果はこのように。
[2018-08-07 12:38:30] [0.86ms] SELECT * FROM
foo
WHEREfoo
.id
= '1'
[1 rows affected or returned ]
{1 foo1 []}
Foo.BarIDs
に join した結果の値を設定したい。
Related を使用する
Associations Has Many にあるように、foo のデータがすでに手元にある場合、または foo のプライマリキーが手元にあって、foo 自体は読み取らなくていいが foobar がほしいときなどには Related
が使えそうだ。
foo の値(今回の場合 name
)もほしい場合は、別途 Find
なり First
なりしないといけない。
func getFooAssociations(db *gorm.DB) {
foo := Foo{ID: 1}
if err := db.First(&foo).Error; err != nil {
log.Fatal(err)
}
if err := db.Model(&foo).Related(&foo.FooBars).Error; err != nil {
log.Fatal(err)
}
fmt.Println(foo)
}
[2018-08-07 12:47:21] [0.85ms] SELECT * FROM
foo
WHEREfoo
.id
= '1'
[1 rows affected or returned ][2018-08-07 12:47:21] [0.69ms] SELECT * FROM
foobar
WHERE (foo_id
= '1')
[3 rows affected or returned ]
{1 foo1 [{1 1} {1 2} {1 3}]}
Preload を使用する
それぞれ別クエリを投げるのであれば、 Preloadでもシンプルに書けそう。
func preloadFoobar(db *gorm.DB) {
foo := Foo{ID: 1}
if err := db.Preload("FooBars").Find(&foo).Error; err != nil {
log.Fatal(err)
}
fmt.Println(foo)
}
[2018-08-07 12:52:14] [0.87ms] SELECT * FROM
foo
WHEREfoo
.id
= '1'
[1 rows affected or returned ]
[2018-08-07 12:52:14] [0.57ms] SELECT * FROM
foobar
WHERE (foo_id
IN ('1'))
[3 rows affected or returned ]
{1 foo1 [{1 1} {1 2} {1 3}]}
Joins を使用する
ドキュメントに従ってJoinsを使ってみる。
func getFooWithBarIDs(db *gorm.DB) {
query := db.Table("foo").
Select("foo.*, foobar.bar_id").
Joins("left outer join foobar on foo.id = foobar.foo_id").
Where("foo.id = ?", 1)
rows, err := query.Rows()
if err != nil {
log.Fatal(err)
}
var fooWithBarID struct {
Foo
BarID *int
}
var barIDs []int
for rows.Next() {
err := query.ScanRows(rows, &fooWithBarID)
if err != nil {
log.Fatal(err)
}
if fooWithBarID.BarID != nil {
barIDs = append(barIDs, *fooWithBarID.BarID)
}
}
foo := fooWithBarID.Foo
foo.BarIDs = barIDs
fmt.Println(foo)
}
だいぶ、SQLを手で書いている感が出てきたがこのようにするらしい。
rows.Next()
のループの中で foobar.bar_id
を取得して foo.BarIDs
に設定する必要があるのだが、
BarIDs []int
を持つ Foo
とは別に
BarID *int
を持つ fooWithBarID
を定義して、これを ScanRows
に使用する。
int
ではなく *int
にしているのは、 left outer join
なので foobar が無い(bar_id
が NULL)の場合にゼロ値が barIDs
に入らないように)
- foobar に紐付くレコードあり
[2018-08-07 13:25:23] [0.87ms] SELECT foo.*, foobar.bar_id FROM
foo
left outer join foobar on foo.id = foobar.foo_id WHERE (foo.id = '1')
[0 rows affected or returned ]
{1 foo1 [1 2 3]}
- foobar に紐付くレコードなし
[2018-08-07 13:28:23] [0.83ms] SELECT foo.*, foobar.bar_id FROM
foo
left outer join foobar on foo.id = foobar.foo_id WHERE (foo.id = '3')
[0 rows affected or returned ]
{3 foo3 []}
…もうちょっとマシな書き方はないですかね…。
Joins
を使った1クエリに拘らず、大人しく Preload
で2クエリ発行したほうがいいように思えてきた。