So, thought I had this working last night, couldve sworn it. Now it no worky, and I figure its about time to ask for help.
Im defining dynamic fields in the database, semi EAV style, and lets just state right now I dont care to hear your opinions on whether EAV is a good idea or not :)
Anyways Im doing it a little differently than Ive done in past, basically when an attribute (or field), is added, I create a add column to a particular attributes table migration and run it (or a remove it one) -- ANYWAYS, because there is a category layer sitting in the middle which is the direct relationship where all the attributes are defined, I cant use the actual attribute name as a column name, as attributes are category specific.
So, if it helps you visualize
Entity
belongs_to :category
Category
has_many :entities
EntityAttribute
belongs_to :category
EntityAttributeValue
belongs_to :entity_attribute
belongs_to :entity
And EAV table spans horizontally as new attributes are created, with columns labeled attribute_1 attribute_2, which contain the values for that particular entity.
Anyways -- I am trying to make the methods dynamic on the entity model, so I can call @entity.actual_attribute_name, rather than @entity.entity_attribute_value.field_5
Here is the code I thought was working --
def method_missing(method, *args)
return if self.project_category.blank?
puts "Sorry, I don't have #{method}, let me try to find a dynamic one."
puts "let me try to find a dynamic one"
keys = self.project_category.dynamic_fields.collect {|o| o.name.to_sym }
if keys.include?(method)
field = self.project_category.dynamic_fields.select { |field| field.name.to_sym == method.to_sym && field.project_category.id == self.project_category.id }.first
fields = self.project_category.dynamic_field_values.select {|field| field.name.to_sym == method }
self.project_category_field_value.send("field_#{field.id}".to_sym, *args)
end
end
Then today as I went back to code, I realized although I could set the attribute in rails console, and it would return the correct field, when I saved the record, the EntityAttributeValue was not being updated (represented as self.project_category_field_value, above.)
So after looking into it further it looked like I just had to add a before_update or before_save callback to manually save the attribute, and thats where I noticed, in the callback, it would re run the method_missing callback, as if the object was being duplicated (and the new object was copy of original object), or something, Im not quite sure. But at somepoint during save process or before, my attribute dissapears into oblivion.
So, well I guess I half way answered my own question after typing it out, I need to set an instance variable and check to see if it exists at the beginningish of my method_missing method (right?) Maybe that's not what's happening I dont know, but Im also asking if there is a better way of doing what I am trying to do.
And if using method_missing is a bad idea, please explain why, as going through posts regarding method missing I heard some people slamming it but not one of those people bothered offering a reasonable explanation as to why method missing was a bad solution.
Thanks in advance.
You can look at my presentation where I described how to delegate methods to associated models EAV with ActiveRecord
For example we use STI for our Product models and we have associated Attribute models for them.
At first we create the abstract Attribute model
class Attribute < ActiveRecord::Base
self.abstract_class = true
attr_accessible :name, :value
belongs_to :entity, polymorphic: true, touch: true, autosave: true
end
Then all our attribute models are inherited from this class.
class IntegerAttribute < Attribute
end
class StringAttribute < Attribute
end
Now we need to describe the base product class
class Product < ActiveRecord::Base
%w(string integer float boolean).each do |type|
has_many :"#{type}_attributes", as: :entity, autosave: true, dependent: :delete_all
end
def eav_attr_model(name, type)
attributes = send("#{type}_attributes")
attributes.detect { |attr| attr.name == name } || attributes.build(name: name)
end
def self.eav(name, type)
attr_accessor name
attribute_method_matchers.each do |matcher|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{matcher.method_name(name)}(*args)
eav_attr_model('#{name}', '#{type}').send :#{matcher.method_name('value')}, *args
end
EOS
end
end
end
So we added the #eav_attr_model
method which is a proxy method to our associated models and the .eav
method which generates attribute methods.
It's all. Now we can create our product models which are inherited from Product class.
class SimpleProduct < Product
attr_accessible :name
eav :code, :string
eav :price, :float
eav :quantity, :integer
eav :active, :boolean
end
Usage:
SimpleProduct.create(code: '#1', price: 2.75, quantity: 5, active: true)
product = SimpleProduct.find(1)
product.code # "#1"
product.price # 2.75
product.quantity # 5
product.active? # true
product.price_changed? # false
product.price = 3.50
product.code_changed? # true
product.code_was # 2.75
if you need a a more complex solution which allows to create attributes in runtime or use query methods to obtain data you can look at my gem hydra_attribute which implements the EAV for active_record models.
That's some seriously intense programming to be going on in the method_missing
department. What you should have is something more like this:
def method_missing(name, *args)
if (method_name = dynamic_attribute_method_for(name))
method_name.send(*args)
else
super
end
end
You can then try and break this down into two parts. The first is creating a method that decides if it can handle a call with a given name, here dynamic_attribute_method_for
, and the second is the actual method in question. The job of the former is to ensure the latter works by the time it is called, possibly using define_method
to avoid having to go through all of this again the next time you access the same method name.
That method might look like this:
def dynamic_attribute_method_for(name)
dynamic_attributes = ...
type = :reader
attribute_name = name.to_s.sub(/=$/) do
type = :writer
''
end
unless (dynamic_attributes.include?(attribute_name))
return
end
case (type)
when :writer
define_method(name) do |value|
# Whatever you need
end
else
define_method(name) do
# Whatever you need
end
end
name
end
I can't tell what's going on in your method as the structure is not clear and it seems highly dependent on the context of your application.
From a design perspective you might find it's easier to make a special-purpose wrapper class that encapsulates all of this functionality. Instead of calling object.attribute_name
you'd call object.dynamic_attributes.attribute_name
where in this case dynamic_attributes
is created on demand:
def dynamic_attributes
@dynamic_attributes ||= DynamicAccessor.new(self)
end
When that object is initialized it will pre-configure itself with whatever methods are required and you won't have to deal with this method missing stuff.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With