开发者

How can I extend an object returned from an ActiveRecord association at runtime?

I have a model as follows:

class Property < ActiveRecord::Base
  has_and_belongs_to_many :property_values
end

What I would like to do is to extend any value returned by a find on the property_values extension with a module that is determined by an attribute of the Property object. I've attempted something like this:

class Property < ActiveRecord::Base
  has_and_belongs_to_many :property_values, :extend => PropertyUtil::Extensible

  def enrich(to_extend)
    modules.split(/\s*,\s*/).each do |mod|
      to_extend.extend(Properties.const_get(mod.to_sym))
    end
  end
end

module Pr开发者_高级运维opertyUtil
  module Extensible
    def self.extended(mod)
      mod.module_eval do
        alias old_find find
      end
   end

    def find(*args)
      old_find(*args).map{|prop| proxy_owner.enrich(prop)}
    end
  end
end

Where all modules that may be selected are defined in the Properties module. In attempting to run with this code, though, there are a couple of problems; first, to my surprise, none of the dynamic finders (property_values.find_by_name, etc.) appear to delegate to find; second, something with how I've done the aliasing leads to a stack overflow when I try to run the find directly.

Is there a way to do what I'm attempting? What method can I alias and override such that all results returned by the association extension, irrespective of how they are retrieved, are extended with the appropriate modules?

Thanks, Kris


I never tried to do this but you may want to try the following (I just changed how the aliases are done):

class Property < ActiveRecord::Base
  has_and_belongs_to_many :property_values, :extend => PropertyUtil::Extensible

  def enrich(to_extend)
    modules.split(/\s*,\s*/).each do |mod|
      to_extend.extend(Properties.const_get(mod.to_sym))
    end
  end
end

module PropertyUtil
  module Extensible
    def self.extended(mod)
      mod.module_eval do
        alias_method :old_find, :find
        alias_method :find, :new_find
      end
   end

    def new_find(*args)
      old_find(*args).map{|prop| proxy_owner.enrich(prop)}
    end
  end
end

If it does not work here is another idea you may wanna try:

class Value < ActiveRecord::Base
  self.abstract_class = true

end


class ExtendedValue < Value

end

class ExtendedValue2 < Value

end


class Property < ActiveRecord::Base
  has_and_belongs_to_many :property_values, :class_name => 'ExtendedValue'
  has_and_belongs_to_many :property_values_extended, :class_name => 'ExtendedValue'
  has_and_belongs_to_many :property_values_extended2, :class_name => 'ExtendedValue2'


end

The idea is to have one hatbm association per "type" (if you can group your extensions that way) and use the one you want at a given time, if you can do what you want that way I am also pretty sure it will have a smaller impact performance than patching every returned object after activerecord returned them.

I am kinda curious at what you are trying to achieve with this :)


It is much easier to simply use classes to change the functionality. You can have classes of PropertyValues with the appropriate behavior and use either STI (Single Table Inheritance) to instantiate the appropriate instance or you can over-ride the 'instantiate' ActiveRecord class method to set the class using the #becomes instance method:

class PropertyValue < AR:Base
  def self.instantiate(record)
    property_value = super
    case property_value.sub # criteria for sub_class
      when 'type1' then property_value.becomes(Type1)
      when 'type2' then property_value.becomes(Type2)
    end
   end
end 

class Type1 < PropertyValue
  def some_method
    # do Type1 behavior
  end
end

class Type2 < PropertyValue
  def some_method
    # do Type2 behavior
  end
end

I have found that using classes and inheritance provides much cleaner, simpler code and is easier to test.


I ended up using an after_find call on the value class to resolve this problem. This is a pretty suboptimal solution, because it means that the module information ends up needing to be duplicated between the property referent and the value, but it's workable, if less than exactly performant. The performance hit ended up being large enough that I had to cache a bunch of data in the database with the results of computations over large numbers of properties, but this turned out not to be all bad, in that it simplified the process for extraction of report data considerably.

In the end, here are some bits of what I ended up with:

module Properties::NamedModules
  def modules
    (module_names || '').split(/\s*,\s*/).map do |mod_name|
      Property.const_get(mod_name.demodulize.to_sym)
    end
  end
end

module Properties::ModularProperty
  def value_structure
    modules.inject([]){|m, mod| m + mod.value_structure}.uniq
  end
end

module Properties::Polymorphic
  include NamedModules, ModularProperty

  def morph
    modules.each {|mod| self.extend(mod) unless self.kind_of?(mod)}
  end 
end 

class Property < ActiveRecord::Base
  include Properties::NamedModules, Properties::ModularProperty

  has_and_belongs_to_many :property_values, :join_table => 'property_value_selection' 

  def create_value(name, value_data = {})
    property_values.create( 
      :name => name,
      :module_names => module_names,
      :value_str => JSON.generate(value_data) 
    )
  end
end

class PropertyValue < ActiveRecord::Base
  include Properties::Polymorphic
  has_and_belongs_to_many :properties, :join_table => 'property_value_selection'
  after_find :morph
end
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜