gormでmany-to-manyを扱う時、リレーション先のテーブルの内容で絞り込みをする

gormで以下のようなmany-to-manyの関係を扱う時、Tagの内容で絞り込む方法がよくわからず苦戦したので記録しておく。

type Post struct {
	gorm.Model
	Title string `gorm:"column:title"`
	Tags  []Tag  `gorm:"many2many:post_tags"`
}

type Tag struct {
	gorm.Model
	Name  string `gorm:"column:name"`
	Posts []Post `gorm:"many2many:post_tags"`
}

まずは全レコード取得する場合だが、例えばこう書くと思う。

db.Debug().Find(&posts)

これを実行すると以下のようにdeleted_atがNULLのレコードを全て取得する。

[1.677ms] [rows:2] SELECT * FROM `posts` WHERE `posts`.`deleted_at` IS NULL
{Model:{ID:1 CreatedAt:2022-01-05 03:49:24.753 +0000 UTC UpdatedAt:2022-01-05 03:49:24.757 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-1 Tags:[]}
{Model:{ID:2 CreatedAt:2022-01-05 03:49:24.765 +0000 UTC UpdatedAt:2022-01-05 03:49:24.769 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-2 Tags:[]}

次に、リレーションがあるテーブルの情報も構造体にマッピングする場合だが、Eager Loadingを使ってこう書く。

db.Debug().Preload("Tags").Find(&posts)

これを実行すると、以下のようにTagsにデータが入っていることがわかる。

2022/01/05 14:10:55 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:151
[2.292ms] [rows:2] SELECT * FROM `post_tags` WHERE `post_tags`.`post_id` IN (1,2)

2022/01/05 14:10:55 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:151
[2.327ms] [rows:2] SELECT * FROM `tags` WHERE `tags`.`id` IN (1,2) AND `tags`.`deleted_at` IS NULL

2022/01/05 14:10:55 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:151
[6.615ms] [rows:2] SELECT * FROM `posts` WHERE `posts`.`deleted_at` IS NULL
{Model:{ID:1 CreatedAt:2022-01-05 03:49:24.753 +0000 UTC UpdatedAt:2022-01-05 03:49:24.757 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-1 Tags:[{Model:{ID:1 CreatedAt:2022-01-05 03:49:24.743 +0000 UTC UpdatedAt:2022-01-05 03:49:24.743 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-1 Posts:[]}]}
{Model:{ID:2 CreatedAt:2022-01-05 03:49:24.765 +0000 UTC UpdatedAt:2022-01-05 03:49:24.769 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-2 Tags:[{Model:{ID:2 CreatedAt:2022-01-05 03:49:24.748 +0000 UTC UpdatedAt:2022-01-05 03:49:24.748 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-2 Posts:[]}]}

さて、問題はここからで、Tagsの中身で絞り込む場合。例えば、tag-2 のタグと関連のあるPostだけほしいような場合。いろいろ調べた結果、以下のようにPreloadとJOINを組み合わせることで実現できた。

db.Debug().
	Preload("Tags").
	Joins("JOIN post_tags ON post_tags.post_id = posts.id").
	Joins("JOIN tags ON post_tags.tag_id = tags.id").
	Where("tags.name in (?)", []string{"tag-2"}).
	Find(&posts)

実行すると以下のようにクエリが発行される。

2022/01/05 14:14:43 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:184
[1.978ms] [rows:1] SELECT * FROM `post_tags` WHERE `post_tags`.`post_id` = 2

2022/01/05 14:14:43 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:184
[1.952ms] [rows:1] SELECT * FROM `tags` WHERE `tags`.`id` = 2 AND `tags`.`deleted_at` IS NULL

2022/01/05 14:14:43 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:184
[7.030ms] [rows:1] SELECT `posts`.`id`,`posts`.`created_at`,`posts`.`updated_at`,`posts`.`deleted_at`,`posts`.`title` FROM `posts` INNER JOIN post_tags ON post_tags.post_id = posts.id INNER JOIN tags ON post_tags.tag_id = tags.id WHERE tags.name in ('tag-2') AND `posts`.`deleted_at` IS NULL
{Model:{ID:2 CreatedAt:2022-01-05 03:49:24.765 +0000 UTC UpdatedAt:2022-01-05 03:49:24.769 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-2 Tags:[{Model:{ID:2 CreatedAt:2022-01-05 03:49:24.748 +0000 UTC UpdatedAt:2022-01-05 03:49:24.748 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-2 Posts:[]}]}

2022-01-07 追記:tags.nameで複数指定した場合、SQL的には複数のレコードが結果として返るが、素朴に構造体にマッピングされてしまい重複することに後で気づいた。

db.Debug().
	Preload("Tags").
	Joins("JOIN post_tags ON post_tags.post_id = posts.id").
	Joins("JOIN tags ON post_tags.tag_id = tags.id").
	Where("tags.name in (?)", []string{"tag-1", "tag-2"}).
	Find(&posts)

これを実行すると以下のようになり、ID:5の構造体がスライス内に2つあることがわかる。これだとGoからは利用しにくい。


2022/01/07 14:57:25 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:190
[2.606ms] [rows:4] SELECT * FROM `post_tags` WHERE `post_tags`.`post_id` IN (3,5,4)

2022/01/07 14:57:25 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:190
[2.348ms] [rows:2] SELECT * FROM `tags` WHERE `tags`.`id` IN (3,4) AND `tags`.`deleted_at` IS NULL

2022/01/07 14:57:25 /Users/ryotakaishi/src/github.com/takaishi/gorm_practice/main.go:190
[8.796ms] [rows:4] SELECT `posts`.`id`,`posts`.`created_at`,`posts`.`updated_at`,`posts`.`deleted_at`,`posts`.`title` FROM `posts` JOIN post_tags ON post_tags.post_id = posts.id JOIN tags ON post_tags.tag_id = tags.id WHERE tags.name in ('tag-1','tag-2') AND `posts`.`deleted_at` IS NULL
{Model:{ID:3 CreatedAt:2022-01-07 01:19:28.754 +0000 UTC UpdatedAt:2022-01-07 01:19:28.758 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-1 Tags:[{Model:{ID:3 CreatedAt:2022-01-07 01:19:28.744 +0000 UTC UpdatedAt:2022-01-07 01:19:28.744 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-1 Posts:[]}]}
{Model:{ID:5 CreatedAt:2022-01-07 01:19:28.777 +0000 UTC UpdatedAt:2022-01-07 01:19:28.782 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-3 Tags:[{Model:{ID:3 CreatedAt:2022-01-07 01:19:28.744 +0000 UTC UpdatedAt:2022-01-07 01:19:28.744 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-1 Posts:[]} {Model:{ID:4 CreatedAt:2022-01-07 01:19:28.749 +0000 UTC UpdatedAt:2022-01-07 01:19:28.749 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-2 Posts:[]}]}
{Model:{ID:4 CreatedAt:2022-01-07 01:19:28.766 +0000 UTC UpdatedAt:2022-01-07 01:19:28.77 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-2 Tags:[{Model:{ID:4 CreatedAt:2022-01-07 01:19:28.749 +0000 UTC UpdatedAt:2022-01-07 01:19:28.749 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-2 Posts:[]}]}
{Model:{ID:5 CreatedAt:2022-01-07 01:19:28.777 +0000 UTC UpdatedAt:2022-01-07 01:19:28.782 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Title:article-3 Tags:[{Model:{ID:3 CreatedAt:2022-01-07 01:19:28.744 +0000 UTC UpdatedAt:2022-01-07 01:19:28.744 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-1 Posts:[]} {Model:{ID:4 CreatedAt:2022-01-07 01:19:28.749 +0000 UTC UpdatedAt:2022-01-07 01:19:28.749 +0000 UTC DeletedAt:{Time:0001-01-01 00:00:00 +0000 UTC Valid:false}} Name:tag-2 Posts:[]}]}

gormの機能でどうにかできないか軽く調べたが見つからなかった。とりあえず、ワークアラウンドとしては自分で重複排除する方法がある。

m := map[uint]bool{}
uniq := []Post{}
for _, post := range posts {
	if !m[post.ID] {
		m[post.ID] = true
		uniq = append(uniq, post)
	}
}

このようにすればよい。