下記のように1対1対多の関係のモデルがあるとします。
- UserとExamineeは1対1
- ExamineeとTestは1対多
class User
has_one :examinee
end
class Examinee
belongs_to :user
has_many :tests
end
class Test
belongs_to :examinee
end
では、Userモデルから関連するTestモデルを取得したいときはどのように実装しますか?
様々なやり方がありますが、ActiveRecordの便利機能delegate
を使うか、has_many-through
を使うことが多いのではないでしょうか?
どちらもやりたいことは達成できますが、発行されるクエリが少し違うので紹介します。
delegate
delegateを使うとメソッドを別クラスに委譲することが出来ます。
詳細はRailsガイドを参照してください。
3.4.1 delegate
今回の場合、下記のように実装します。
delegate :tests, to: :examinee
実行すると下記の通り2つのクエリーが発行されます。
まず委譲先のexamineeを取得(1つ目のクエリー)して、その後、examinee.testsを実行(2つ目のクエリー)する挙動になっています。
irb> user.tests
Examinee Load SELECT `examinees`.* FROM `examinees` WHERE `examinees`.`user_id` = 1 LIMIT 1
Test Load SELECT `tests`.* FROM `tests` WHERE `tests`.`examinee_id` = 1
has_many-through
has_many-throughは多対多の時に使われることが多いですが、今回のように1対多の場合も利用できます。
詳細はRailsガイドを参照してください。
2.4 has_many :through 関連付け
今回の場合、下記のように実装します。
has_many :tests, through: :examinee
実行すると下記の通り1つのクエリーが発行されます。
こちらの場合はjoinしたクエリーが1つだけ発行されます。
この機能が多対多に対応するように実装されていると考えると、deletgateのように2段階では効率よく取得できないのでjoinで取得しているんだなと理解できると思います。
irb> user.tests
Test Load SELECT `tests`.* FROM `tests` INNER JOIN `examinees` ON `tests`.`examinee_id` = `examinees`.`id` WHERE `examinees`.`user_id` = 1
最後に
2クエリーで取得するほうが良いのか、joinされた1クエリーで取得するほうが良いのかは実行環境によるので一概に良し悪しは判断出来ません。
というか大抵の場合はどちらで書いても問題なく動作するのでぶっちゃけどちらでもよいと思います。
ただ、ブラックボックス的に見ると同じことをしているように見えても、今回のように内部で発行されるクエリーが違ったりします。
たまにはこういう細かな違いを機能の成り立ちや目的などを考えならが確認してみると面白いと思います。