🚋
Rails でポリモーフィック先のリレーションを includes する時に同名のリレーションを特定のクラスだけで先読みしたい
ふとしです。
以下は都合上 rails 6.1.7
で確認しています。
前提
-
Foo
FooResource
Bar
Baz
Qux
というクラスがあります。 -
Foo
はFooResource
を中間テーブルとしてポリモーフィックを利用してBar
Baz
をhas_many
で持ちます。 -
Bar
Baz
はQux
をbelongs_to
で持ちます。
ポリモーフィック先の先まで includes できる
Bar
Baz
の Qux
を利用する場合、以下のようにして includes
で先読みすることができます。
Foo.
includes(foo_resources: { resource: :qux })
Foo Load (0.4ms) SELECT `foos`.* FROM `foos`
FooResource Load (0.6ms) SELECT `foo_resources`.* FROM `foo_resources` WHERE `foo_resources`.`foo_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Bar Load (0.4ms) SELECT `bars`.* FROM `bars` WHERE `bars`.`id` IN (98, 41, 74, 78, 47, 22, 53, 26, 33, 11, 10, 61, 52, 54, 89, 28, 19, 77, 70, 27, 16, 45, 13)
Baz Load (0.4ms) SELECT `bazs`.* FROM `bazs` WHERE `bazs`.`id` IN (53, 56, 27, 8, 39, 71, 67, 20, 91, 54, 98, 93, 30, 26, 66, 58)
Qux Load (0.4ms) SELECT `quxes`.* FROM `quxes` WHERE `quxes`.`id` IN (77, 89, 18, 48, 81, 45, 49, 61, 29, 73, 25, 27, 78, 84, 41, 80, 67, 40, 5, 47)
Qux Load (0.4ms) SELECT `quxes`.* FROM `quxes` WHERE `quxes`.`id` IN (32, 61, 7, 38, 58, 3, 70, 72, 55, 33, 24, 46, 18, 92, 89, 97)
preload
の都合上? Bar
用と Baz
用で Qux
が 2 分割されていますが、バラバラでロードされるよりはマシですね。マシです。
Bar 側でしか Qux を使わない場合はどうすればいいか?
Baz
側は不要な場合はどうすればよいでしょうか?
深いデータ構造だと無駄な先読みが見逃せないコストになるかもしれません。
別名の belongs_to を用意する
- 名前が他と被らない
belongs_to
を追加する。 -
includes
を変える。- 追加した
qux_for_bar
はBaz
に存在しませんが、エラーにはなりません。
- 追加した
- 使用箇所では
resource.qux_for_bar
とする。
ことで、Bar
の qux
のみ先読みするようにできました。
class Bar < ApplicationRecord
belongs_to :qux
belongs_to :qux_for_bar, class_name: 'Qux', foreign_key: 'qux_id'
end
Foo.
includes(foo_resources: { resource: :qux_for_bar })
Foo Load (0.5ms) SELECT `foos`.* FROM `foos`
FooResource Load (0.6ms) SELECT `foo_resources`.* FROM `foo_resources` WHERE `foo_resources`.`foo_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Bar Load (0.4ms) SELECT `bars`.* FROM `bars` WHERE `bars`.`id` IN (98, 41, 74, 78, 47, 22, 53, 26, 33, 11, 10, 61, 52, 54, 89, 28, 19, 77, 70, 27, 16, 45, 13)
Baz Load (0.3ms) SELECT `bazs`.* FROM `bazs` WHERE `bazs`.`id` IN (53, 56, 27, 8, 39, 71, 67, 20, 91, 54, 98, 93, 30, 26, 66, 58)
Qux Load (0.3ms) SELECT `quxes`.* FROM `quxes` WHERE `quxes`.`id` IN (77, 89, 18, 48, 81, 45, 49, 61, 29, 73, 25, 27, 78, 84, 41, 80, 67, 40, 5, 47)
手動で preload する
-
includes
をresource
で留める。 - ロード後に手動で
preload
する。
ことで、Bar
の qux
のみ先読みするようにできました。
Foo.
includes(foo_resources: :resource).
tap { |foos|
foos.
flat_map(&:foo_resources).
map(&:resource).
filter { |resource| resource.is_a?(Bar) }.
tap { |resources|
ActiveRecord::Associations::Preloader.
new.
preload(resources, :qux)
}
}
Foo Load (0.4ms) SELECT `foos`.* FROM `foos`
FooResource Load (0.4ms) SELECT `foo_resources`.* FROM `foo_resources` WHERE `foo_resources`.`foo_id` IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
Bar Load (0.5ms) SELECT `bars`.* FROM `bars` WHERE `bars`.`id` IN (98, 41, 74, 78, 47, 22, 53, 26, 33, 11, 10, 61, 52, 54, 89, 28, 19, 77, 70, 27, 16, 45, 13)
Baz Load (0.4ms) SELECT `bazs`.* FROM `bazs` WHERE `bazs`.`id` IN (53, 56, 27, 8, 39, 71, 67, 20, 91, 54, 98, 93, 30, 26, 66, 58)
Qux Load (0.4ms) SELECT `quxes`.* FROM `quxes` WHERE `quxes`.`id` IN (5, 25, 67, 73, 45, 77, 49, 89, 41, 78, 84, 47, 29, 81, 40, 80, 61, 48, 27, 18)
まとめ
別名 belongs_to
の方が楽そうでした。
もっと別の正しい方法 (新しいバージョンの Rails でも) をご存知の方がいらっしゃいましたら、ご一報いただけると幸いです。
Discussion