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)
	}
}

このようにすればよい。

2021年ふりかえり

大きなイベントとしては転職と結婚。2020年に転職したのだが、2021年も転職した年になった。転職があると仕事も私生活もしばらくバタバタするが、年末になってまあ落ち着いてきたかなというところ。結婚については書類周りに手こずったのが印象的だった。

Output

特に下半期以降、技術系のブログはあまり書いていない。その一方、CloudNativeDaysの運営の一環でオンラインカンファレンスのプラットフォーム開発をほぼ1年間継続していた。常に利用される性質のサービスではないとしても、仕事以外で一つのサービスを継続して開発・運用した経験は大きい。アーキテクチャや機能デザイン、データ構造などを考えることも多く良かった。

Presentation

2021年は登壇なし。2022年は今やっていることをまとめて外に出せるといいなとは思っている。

Writing

毎日Scrapboxに日記をつける習慣がついた。と言っても大したことは書いてないのだが。とりあえずその日のページを作って、それまではTwitterに書いていたようなことを書いている。ブログに書いたテック系の記事は以下。少ない…。

Community

CloudNativeDaysの実行委員会に関わり、以下3つのオンラインカンファレンスの開催を手伝った。

  • CloudNativeDays OnlineSpring 2021
  • CI/CD Conference 2021
  • CloudNativeDays Tokyo 2021

Input

漫画はかなり読んだけど書籍はほとんど読めなかった気がする。あまり記録も取れていない。

英語の学習はLINEを退職してから止まってしまった。

新しい技術としてはTypeScriptを少し使えるようになった。また、Kafka・Cassandra・Presto・Hive・BigQueryなど、データ寄りの技術に触れたのは大きい。ECSも触ってKubernetesもECSもそれなりに使えるマンになった。

Podcast

1年間、キマグレエフエムの収録を継続した。これはすごくうれしいことだなあ。

Health

72kgくらいまで増量した後、減量を開始した。年末時点で62〜63kg位まで下がっていて、やれば減量できるなという実感を得た。上半期は割と自宅トレーニングをやっていたが、下半期はジムの契約をしてそちらに週4で通っている。習慣化されてきていていい感じ。

2020年〜2021年前半は睡眠の質があまり良くなかったが、2022年後半に結構改善されてきたように思う。

その他

年末の話になるが、カーシェアを契約した。買い物に使ったりしたが、もっと早く契約して使えば良かったな〜と思った。後、副業案件が増えてきたので個人事業主の届け出を出した。

2021年買ってよかったもの

毎年恒例の買ってよかったもの2021年番。年末にまとめて書くと下半期に買ったものの印象が強くなりがち。

OuraRing

2021年3月に購入。ほぼ毎日、風呂に入るときやトレーニングでバーベルを扱う時など以外はずっとつけている。睡眠トラッキング機能が特に気に入っている。精度も高いと感じるし、僕はAppleWatchのような時計型のセンサーを手首につけて寝ると気になっていたのだが、指輪型だと全然気にならなくて快適なのもよい。難点としては指輪としてはやや大きいこと、トレーニングでバーベルやダンベルを持つとリングが当たって傷がついたり指輪内側のセンサーが肌に食い込んで痛かったりするところ。とはいえ難点より圧倒的に利点が大きいので今後も使い続けたい。11月に3世代目が登場したのだが、それも購入した。50ドル値引きと6ヶ月サブスクが無料になるクーポンが後1つ残っているので、知人で買おうと思っている人は連絡ください。

https://ouraring.com/

ROTOTO WASHI PILE CREW SOCKS

2021年の秋冬用靴下はこれで決定。3月に靴下を何種類か買って試していて、これが一番よかったので買い足してローテーションできるようにした。この靴下のいいところとしては、まず生地がやや厚めにもかかわらず蒸れにくいところがある。てきとうな靴下だと少し歩くだけで蒸れて不快なのだが、ROTOTOのWASHI PILE SOCKSはそれがないところがよい。次に、つま先の縫い目がフラットで履き心地がよい。適当に靴下を買うと大抵つま先の縫い目がフラットではなく、靴を履いた時に気になってしまう。この靴下はそれがない。縫い目に使われている糸が靴下全体の色とは違っていて、アクセントになっているのもよい。靴を履いていると見えない箇所だが、そういう箇所にこだわっているところが粋じゃないか。靴下全体の生地はベースカラーに白色が差し込まれていて、柄物とまではいかないが単色でもなく、ほどよくカジュアルなところもよい。派手な柄が入っている靴下はあまり好みではないのだが、全くの単色も面白みに欠ける。まさにちょうどいい具合だと思う。最後に、履き口が締め付けてこないところが素晴らしい。脱いだときにあまり跡がつかず、履いていて気持ちいい。まだまだ家にいる時間が長いので、こういうゆったりした靴下がとてもあう。

https://www.rototo.jp/

https://www.rototo.jp/products/r1066

Roborock S6 MaxV

水拭きできるロボット掃除機。結構古いルンバから買い換えたのだが、水拭きがとにかくよい。ブラシでゴミを集めるだけだと微細なホコリやチリが残っていることが多く、素足で歩いたり床に座ると気になっていた。しかし水拭きは非常に面倒で、正直あまりやりたくない・やらない家事の一つ。Roborock S6 MaxVを買ってからは毎日水拭きできて、とても床が気持ちよくなった。新モデルのS7+/S7が水拭き機能が強化されていて気になっているのはないしょ。

https://www.roborock.jp/products/s6-maxv

BODY WILD AIRS WARM BOAT NECK

今シーズンは冬用のインナーとしてヒートテックではなくBODY WILD AIRS WARMにした。ヒートテックだとカットソーの襟から見えることがあるのだが、今回買ったこれはボートネックタイプなので見えないのがよい。また、ヒートテックは家の中で動かないでいるとあまり暖かくない木がするのだが、AIRS WARMは暖かいと思う。肌触りもよい。これの上に無印のフランネルシャツを着ていると、アウターはそこまで厚くなくても今の時期なら夜でもそこそこ暖かい。

https://amzn.to/3oJyY2w

掃除用ブラシセット

これまでは1本だけ買って掃除のたびにどこにおいたっけ?って探したり、使い古した歯ブラシ使ったりしていた。ふと思い立って何本かセットで買ってみたのだが、大正解だった。よく使うところに1本ずつ置いておけば掃除したいときにすぐ掃除できて非常によい。PROIDEA 大津式 お掃除ブラシ J と まめいた アマゾンオリジナル 先っぽブラシセット を買ったのだが、一番よく使うのはまめいたの先っぽまで毛があるブラシ。棒の先にぐるっとブラシがついているので、シンクの排水口を掃除しやすい。お掃除ブラシJはブラシの幅が非常に狭いので、溝の掃除に重宝している。お掃除ブラシJは正直ちょっと高いと思うのだが、まめいたはリーズナブルだと思う。たぶん100均にも似たようなブラシはあるだろうから、次は100均でまとめ買いするかも。何にせよ、使うところに常備しておくことで掃除のハードルを下げられる、ということが重要です。

https://amzn.to/3rVtvrf

肉体改造のピラミッド 栄養編

今年減量した(今もしている)のだが、減量ペースや摂取カロリー量の決定をどのように行うかにおいて非常に参考にした本。おかげで順調に減量が進んでいる。

https://amzn.to/3GAXrNs

Mammut Ayako Pro HS Hooded Jacket AF Men

山に行くわけではないけどアウトトアジャケットがほしくて、黒いものをいろいろ探していて見つけたものがこれ。見つけたのは2021年の頭頃で、購入は秋頃。基本は黒ベースでデザインされていて、ほんの少しだけオレンジのワンポイントが入っていて非常にシンプルなのがよい。ブランドロゴも黒ベースで目立たない。ゴアテックス素材なのだが、内側はメッシュ生地なので着心地もよい。ゴアテックス単体のジャケットに比べると蒸れにくい気もする。黒ベースのアウトドアジャケットだとアークテリクスがあるけど、結構高いしファスナーが左差しでちょっと使いにくい。記事書いてる時に調べたら、黒のモデルがなくなっているように見える…


結婚した

今日、婚姻届を提出して結婚した。5年前は自分が結婚するとは想像もしていなかったけど、人生何があるかわからないものだ。

戸籍謄本の取得はマイナンバーカードを使って割と楽にできたが、証人欄へのサインのために郵送が必要でそこは少し大変だった。どちらかというと大変なのは姓が変わったパートナーで、変更の手続きあれこれもそうだし、旧姓併記のための手続きを聞いてピタゴラスイッチぶりに眩暈がした。

特に生活面で変わることはないですが、今後ともよろしくお願いします。アドバイスなどあれば是非ご教授ください。Twitterなりメールなりまたは下記のリスト経由でも大丈夫です。

https://www.amazon.jp/hz/wishlist/ls/Z4S7GXB6QH5S?ref_=wl_share

新型コロナウイルスワクチン接種記録

7/29: 1回目の接種

自治体の大規模接種会場で接種。接種した肩の痛みだけで、発熱はなし。

8/7: モデルナアーム

めちゃくちゃ肩が赤く腫れて痒くなった。接種後の痛みよりこっちのほうが辛い。塗り薬でかゆみは抑えられたのでそれで対処した。

8/26: 2回目の接種が中止

モデルナワクチンへの異物混入の件で、ちょうど2回目の接種予定日の全接種が中止になってしまった。いろいろ買い込んで仕事も休みにしていたので、肩すかしをくらった形となる。まあ仕方ないのだが。当日は普通の有給に切り替えて仕事は休んだ。

9/9: 2回目の接種

振り替え接種が元々の予定の2週後になったので、9月になってからの接種となった。

  • 注射を打った肩の痛みだけじゃなく、胸や右肩もなんか違和感があった。前日にトレーニングしたからとかか?
  • 37.7度まで上がったなと思ったら突然の悪寒。めちゃ寒く感じてガタガタしていた
  • 熱があるのに汗をかかない不思議
  • 39度まで上がったのでロキソニンを飲む。その後じわじわ汗をかきはじめた
  • 汗はでるし暑いしなぜか鼻がつまるしで寝られない。2時〜3時くらいまで続く。その後は寝られたと思う
  • 翌日、起きたら37.2度まで下がったがその後また上がってきたのと頭痛がきたので再度ロキソニンを飲む。
  • やたらおなかが空いて、ジャンキーなものを食べたくて仕方なくうっかりマクドナルドをデリバリーする
  • その後は徐々に熱が下がる。ただ頭痛が続くのとだるさが続いている。
  • 2日後の朝はもうだるさもなく普通に活動できそう。ただ頭痛はある。
  • 木曜の午前中に接種して、完全に元の体調に戻ったと感じたのは月曜の朝だった。