🚋

Rails でポリモーフィック先のリレーションを includes する時に同名のリレーションを特定のクラスだけで先読みしたい

に公開

ふとしです。

以下は都合上 rails 6.1.7 で確認しています。

前提

  • Foo FooResource Bar Baz Qux というクラスがあります。
  • FooFooResource を中間テーブルとしてポリモーフィックを利用して Bar Bazhas_many で持ちます。
  • Bar BazQuxbelongs_to で持ちます。

ポリモーフィック先の先まで includes できる

Bar BazQux を利用する場合、以下のようにして 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_barBaz に存在しませんが、エラーにはなりません。
  • 使用箇所では resource.qux_for_bar とする。

ことで、Barqux のみ先読みするようにできました。

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 する

  • includesresource で留める。
  • ロード後に手動で preload する。

ことで、Barqux のみ先読みするようにできました。

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