親モデルの永続化していない情報を子モデルで使うには
Ruby on Railsでは、has_many関連を使って親子構造を表すことがよくあります。一般的なオブジェクト指向の親子構造であれば、次のように、親オブジェクトにセットした情報を子オブジェクトから参照できるように実装することは難しくはありません。
例)親の属性accessed_byに値(user)をセットしたら、子でも利用できる
parent.accessed_by = user
...
parent.children each do |child|
p child.accessed_by
# => user が取得できる
end
しかし、ActiveRecordの親子の場合で、上記の例のaccessed_byがデータベースのカラムではなく、処理の流れの中で設定される永続化されないデータである場合は、これを実現するのは簡単ではありません。単純な方法としては、次のようなものが考えられます。
単純な方法. 親に情報をセットしたときに子にセットする
class Parent << ActiveRecord::Base
has_many :children
attr_reader :accessed_by
def accessed_by=(accessed_by)
@accessed_by = accessed_by
children.each{|c| c.accessed_by = accessed_by}
end
end
class Child << ActiveRecord::Base
belongs_to :parent
attr_accessor :accessed_by
end
この方法は、親にデータをセットし、その後、内部的にキャッシュされたchildrenを参照するという場合にはうまくいきます。難点としては、childrenの取得しなおしや、関連のfindなどを使った検索には対応できないという点があります。
childrenを取り直したり、関連のfindをして得た子には、親のaccessed_by値が入っていない
parent.accessed_by = user children = parent.children # このとき得られるchildrenにはaccessed_by値が入っている children = parent.children(true) # 取り直すと入っていない child = parent.children.find(13) # 特定の子を検索で得ると、accessed_by値は入らない
また、気持ちとしては、親の情報は親にだけ持たせて、子からは次のように親の情報を参照したいところですが、ActiveRecordを普通に使うと、parentの子であるchildの親のparentは、オブジェクトとしては別物になってしまうため、それもできません。
parent.accessed_by = user
p parent.children.first.accessed_by # 内部で Child#accessed_byを呼ぶが・・・
class Child < ActiveRecord::Base
belongs_to :parent
def accessed_by
parent.accessed_by
# このとき得られるparentは、parent.childrenとしたときのparentとは別物
end
end
ActiveRecordはこのような処理が苦手といえます。しかし、モデルはビジネスロジックの中心であり、オブジェクト指向的にきれいにロジックを書こうとすると、どうしてもこの問題で困る時があります。いまのところ、一番良いのではないかと思う解法は、次のようなものです。
まず、has_many関連の拡張を使って、findの動作を変更します。このとき、proxy_ownerを使って親オブジェクトにアクセスできるので、accessed_byをコピーすることができます。
class Parent < ActiveRecord::Base
has_many :children do
def find(*args)
result = super
if result.kind_of?(Array)
result.each{|r| r.accessed_by = proxy_owner.accessed_by}
elsif result
result.accessed_by = proxy_owner.accessed_by
end
result
end
end
end
これで、以下のような手順で、childrenからaccessed_byが正しく得られることになります。
parent.accessed_by = user
parent.children.each do |c|
p c.accessed_by # 正しく入っている
end
child = parent.children.find(13)
p child.accessed_by # 正しく入っている
しかし、後からparentのaccessed_byを変更しても、子に反映されないのが問題です。そこで、次のように親のaccessed_by設定メソッドを書き換えます。
class Parent < ActiveRecord::Base
has_many :children do
def find(*args)
result = super
if result.kind_of?(Array)
result.each{|r| r.accessed_by = proxy_owner.accessed_by}
elsif result
result.accessed_by = proxy_owner.accessed_by
end
result
end
end
attr_reader :accessed_by
def accessed_by=(accessed_by)
@accessed_by = accessed_by
children.each{|c| c.accessed_by = accessed_by}
end
end
これで概ね満足な動作になりますが、ひとつ不満なのは、親にaccessed_byを指定すると必ずchildrenが用意される(childrenテーブルが検索される)点です。children.find(13)やchildren.sizeなどだけが必要な場合、これは無駄な処理となってしまいます。そこで、childrenをまだキャッシュしていなければ、childrenをロードしないようにする処理を加えることが考えられます。キャッシュ状態を手軽に調べるために、関連を拡張して、proxy_targetの状態を返すだけのcached?メソッドを作ります。これをみて、親へのデータセットの際に子のデータを書き換えるかどうかを判断します。
class Parent < ActiveRecord::Base
has_many :children do
def find(*args)
result = super
if result.kind_of?(Array)
result.each{|r| r.accessed_by = proxy_owner.accessed_by}
elsif result
result.accessed_by = proxy_owner.accessed_by
end
result
end
def cached?
!proxy_target.empty?
end
end
def accessed_by=(accessed_by)
@accessed_by = accessed_by
children.each{|c| c.accessed_by = accessed_by} if children.cached?
end
end
これで、childrenのロードが必要ないときには行わないようになり、さらに改善されます。







