Ruby on Rails Tips

密結合な親子構造を作るには

Railsでは通常、親子構造で双方向にリンクをはるには、has_many(またはhas_one)とbelongs_toを使います。しかし、このように作られた構造では、親のオブジェクトと、その子オブジェクトからたどった親オブジェクトは、オブジェクトとしては別物になります。

1回のリクエストの間に、親オブジェクトの子を取得し、かつ、子オブジェクトから親を取得するというケースはあまり多くありませんが、例えば、兄弟オブジェクトを辿って計算したいという場合は、このようなActiveRecordを素直に使った親子構造では、コストが大きくなってしまいます。つまり、親からある子を得るのに1回検索し、子から親を得るのに1回検索し、その親からまた子を得るのに1回検索します。これを兄弟を辿る回数繰り返すのはかなりの無駄になります。また、検索時にincludeオプションを使ってもうまく解決できる問題ではありません。

このようなケースでは、親から子へはhas_many関連を使うものの、子から親へは自前で親オブジェクトを辿るという方法が検討できます。

class Parent < ActiveRecord::Base
  has_many :children do
    def find(*args)
      result = super
      if result.kind_of?(Array)
        result.each{|r| r.parent = proxy_owner if r.kind_of?(Child)}
      elsif result.kind_of?(Child)
        result.parent = proxy_owner
      end
      result
    end
  end
end

class Child < ActiveRecord::Base
  attr_accessor :parent
end

このようにすると、次のようなコードを書いても、最初の p.children で検索された子がキャッシュされた上で、その後の兄弟検索の際もそのキャッシュから検索されるため、無駄な検索が起こらない利点があります。

  p = Parent.find(params[:id])
  total_point = 0
  # 子を順番に処理して合計を計算
  p.children.each{|c| total_point += c.calculate }

  class Child < ActiveRecord::Base
    attr_accessor :parent

    # 自分の点数を、兄弟情報を見ながら計算
    def calculate()
      my_position = self.order
      bonus = 0
      # 兄弟を操作してこのノードの点数を計算
      parent.children.each{|c| bonus += c.point if c.position > my_position && c.point > 100}
      self.point + bonus
    end
  end

ただし、この方法では、親からhas_many関連を使って得たオブジェクト以外では親オブジェクトが取得できないので、直接的な検索などで子オブジェクトを得てさらに関連で親を得たい場合は、別の名前でbelongs_toを併用するか、基本的に親経由で子を使うように書く必要があります。次に挙げたのは併用の例です。

class Child < ActiveRecord::Base
  attr_accessor :parent
  belongs_to :stored_parent, :class_name => 'Parent', :foreign_key => 'parent_id'
end