45
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GORM で 1:N で Join したカラムの値を取得する

Posted at

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)

foofoo_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 WHERE foo.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 WHERE foo.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 WHERE foo.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 []}

…もうちょっとマシな書き方はないですかね…。 :thinking:
Joins を使った1クエリに拘らず、大人しく Preload で2クエリ発行したほうがいいように思えてきた。

45
24
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
45
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?