こんにちは、インターネットコンテンツ兼新米エンジニアのTerryです。
日曜日にQiitaを投稿しても伸びないことは知っている。
でもそこは何があったか察してほしい、すまない。
さて本日は、Railsでモデルを4段階joinする方法についてお伝えします。
その過程でjoinsについて分解して解説して、joinsへの理解を深めます。
最初に断っておくと、この書き方は僕の尊敬する先輩エンジニアに教えていただいたもので、僕の備忘と理解を深めるため無断で掲載しようと思った次第です怒られたら削除します。
2段階(孫)とか3段階(ひ孫)は結構あるんですが、4段階(玄孫[やしゃご])はあまりノウハウが転がっていなかったので、こんなやりかたもあるよ、って感じで知っておくとどこかで使えるかもしれません。
4段階、玄孫ですよ。
どれだけ遠いかって言うと、大久保利通の玄孫が麻生太郎です。幕末か。
(画像はUNDER THE VAILさんより引用)
いやいやお前、4段階もjoinするデータベースの設計がおかしい、見直せという、至極真っ当なご意見があるかと思いますが、とはいえ生きていればそういうこともあるよね。こんな大人になっちゃいけません。
さぁ、皆さんのご理解を頂いたところで、早速進めていきましょう。
テーブル構造
今回想定するテーブルはこんな感じです。
class Parent < ApplicationRecord
belongs_to :children
end
class Child < ApplicationRecord
has_many :parents
has_many :grand_sons
has_many :great_grandsons, through: :grandsons
end
class GrandSon < ApplicationRecord
# 中間テーブルです
belongs_to :child
belongs_to :great_grand_son
end
class GreatGrandSon < ApplicationRecord
belongs_to :great_great_grandson
has_many :grand_sons
end
class GreatGreatGrandSon < ApplicationRecord
has_many :great_grand_sons
end
ちょっとアソシエーションが複雑ですが、実際の実装に近いイメージを想定しているためご容赦を。
実際、キレーにhas_manyやbelongs_toが通ってる例だと、実践で使えない気もします。
ちなみに、grand sonは孫、great grand sonはひ孫、great great grand sonは玄孫という意味らしい。
greatが増えると、どんどん世代が深くなるイメージです。英語って表現力が貧弱やな。
ちなみにちなみに、図にするとこんな感じ。
実践のアソシエーションにモデル名を無理やり乗っけたから、関係性がよくわからんことになっている。やばい。
しかし、他にわかりやすいのも思いつかなかったため、コレで行く。オレは正しい。
何がしたいか
今回は、GreatGreatGrandSonのidから、じっちゃんの名にかけて、それに紐づくParentを引きずり出してやりたい。
ちなみに、SQLで書くとこんなかんじ。
INNER JOIN大明神。
SELECT
*
FROM
parents
INNER JOIN
children
ON
children.id = parent.child_id
INNER JOIN
grand_sons
ON
grand_sons.id = children.grand_son_id
INNER JOIN
great_grand_sons
ON
great_grand_sons.id = grand_sons.great_grand_son_id
INNER JOIN
great_great_grand_sons
ON
great_great_grand_sons.id = great_grand_sons.great_great_grand_son_id
AND
great_great_grand_sons.id = {{funfun}}
整理するために、もう一度図で確認しましょう。
クエリだとINNER JOIN連発するだけで、特に難しいことはないです。
しかし、Railsでは結構面倒な書き方をしないといけないので、次で解説します。
Railsでの書き方
さて、先ほどのクエリをRailsでやってみましょう。
結論から言うと、こんな書き方です。
(無駄に長いので、折り返します)
Parent.joins(
child: [
great_grand_sons: :great_great_grand_son
]
).merge(GreatGreatGrandSon.where(id: funfun))
他にも書き方はいろいろありますが、とりあえず今回はこれ。
ちょっとやる気がなくなってきた。つらい。
しかし、頑張ってひとつずつ分解して説明します。
joinsメソッド
最初のjoinsメソッドですが、これは、モデル間の結合を行います。
形としては、こんな感じ。
Model名.joins(繋げたいtable)
joins - リファレンス - - Railsドキュメント
(http://railsdoc.com/references/joins)
つまり、joinsメソッドは、SQLでいうところINNER JOINを行ってくれるわけですね。
INNER JOINと少し違う点は、「条件に何も入れなければ、勝手にアソシエーションを使って紐付けてくれる(id同士を指定しなくても良い)」というところです。
上の例で発行されるクエリがこちら。
SELECT
models.*
FROM
modes
INNER JOIN
tables
ON
models.table_id = tables.id
ちなみに、他にも似たような動きをするメソッドにincludesがあります。
N+1問題に気を遣う場合はincludes使おうね。
似ているようで全然違う!?Activerecordにおけるincludesとjoinsの振る舞いまとめ
(https://qiita.com/south37/items/b2c81932756d2cd84d7d)
ここまでは普通のjoinsなんですが、ちょっとうざいのが中身の条件節です。
Railsで書いたコードをもう一度見てみましょう。
joins下は、こうなっています。
.joins(child: [great_grand_sons: :great_great_grand_son])
気になる点が2つあると思います。
1つ目は、前半の「child: ...」となっている部分(:childじゃないの?)
2つ目は、[great_grand_sons: :great_great_grand_son]のところ。
さて、これら2つのポイントは、実は1つの表現に集約されています。
答えは「ネスト」です。
Model.joins(hoge: :fuga)とした場合、fugaとhogeがINNER JOINされ、その結果がmodelにINNER JOINされます。fugaがhogeにネストされている状態です。
SQLで書くとこうなります。
SELECT
models.*
FROM
models
INNER JOIN
hoges
ON
models.hoge_id = hoges.id
INNER JOIN
fugaes
ON
fugas.hoge_id = fugas.id
この説明でピンときた人もいるかも知れませんが、2つ目の[great_grand_sons: :great_great_grand_son]の部分も同じくネストです。
つまり、2重にネストしているだけの話なんですね。
もう一度、joins下のコードを見てみます。
.joins(child: [great_grand_sons: :great_great_grand_son])
考え方としては、後ろから解決するとわかりやすいです。
まずgreat_grand_sons: :great_great_grand_sonをINNER JOINします。
その結果を、今度はchildとINNER JOINするのです。
おいgrand_sonどこいった、って感じですが、grand_sonは中間テーブルです。
ちゃんとアソシエーション張っていれば、これで問題なく通ります。Railsって便利。
grand_sonが中間テーブルでないパターンを想定する場合は、childではなくgrand_sonにすれば良いです。
さて、これでやりたいことの8割位は説明できました。
SELECT
*
FROM
parents
INNER JOIN
children
ON
children.id = parent.child_id
INNER JOIN
grand_sons
ON
grand_sons.id = children.grand_son_id
INNER JOIN
great_grand_sons
ON
great_grand_sons.id = grand_sons.great_grand_son_id
INNER JOIN
great_great_grand_sons
ON
great_great_grand_sons.id = great_grand_sons.great_great_grand_son_id
/* ここまで */
AND
great_great_grand_sons.id = {{funfun}}
それでは、最後にmergeメソッドに入りましょう。
mergeメソッド
mergeメソッドといっても、2つあります。
ひとつはRuby自体のメソッドで、ハッシュの結合に利用するものです(今回は説明を割愛します)。
merge, merge! (Hash) - Rubyリファレンス - AmiWiki
(https://ref.xaio.jp/ruby/classes/hash/merge)
もうひとつは、ActiveRecordのmergeメソッドです。
今回は言うまでもなくこちらですね。
(mergeってドキュメントないから困る)
mergeは、複数の条件を併合(=マージ)するメソッドです。
ざっくり言うと、直前に行った条件に対して、さらに絞り込みを行いたい時に使います。
SQLでいうと、最後に書くWHERE句的な感じですかね。
ここで、もう一度コードを見ましょう。
Parent.joins(
child: [
great_grand_sons: :great_great_grand_son
]
).merge(GreatGreatGrandSon.where(id: funfun))
今回は、先ほどJOINした結果のParentにさらに絞り込みを掛けるイメージです。
絞り込む条件は、「idがfunfunであるGreatGreatGrandSon」ということですね。
mergeの中の条件は簡単なので説明はいらないと思います。
ちなみに、mergeの一般的な使い方はコレではなく、モデルのscopeを利用するパターンが多いと思います。
scopeについては、こちら。
Ruby On Railsのscopeメソッドで検索を効率化する
(https://programming-beginner-zeroichi.jp/articles/62)
ざっくり言うと、**「よく使う絞り込みを、あらかじめモデル内に設定しておく」**ということですね。
今回はmergeのこの機能を使わず、単に条件を追加する用途で使っています。
モデルによってscopeをうまく設定して使ってあげることで、controllerの記述をシンプルにして、fat_controller化することを防げますので、効率的に使いましょう。
終わりです!
以上、4段階もjoinする話からjoinsとmergeについて説明してみました!
余裕があれば、クエリの実行計画や結果を吐き出して、どのメソッドや書き方が早いのか、4段階JOINで処理が遅くならないかなど検証したいと思います。