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
精彩评论