Ruby on rails dynamic attribute fields from the database using method_missing methods

So, I thought that I had this work last night, I could swear. Now it didn’t work, and I look forward to asking for help.

Im defining dynamic fields in a database, semi EAV-style, and allows you to simply declare right now. I do not want to hear your opinions on whether EAV is a good idea or not :)

In any case, I do it a little differently than Ive done in the past, basically when an attribute (or field) is added, I create an add column to migrate the table of specific attributes and run it (or delete it) - In any case, since there is a category layer in the middle, which is a direct link where all attributes are defined, I cannot use the actual attribute name as the column name, since the attributes belong to the category.

So, if that helps you visualize

Entity belongs_to :category Category has_many :entities EntityAttribute belongs_to :category EntityAttributeValue belongs_to :entity_attribute belongs_to :entity 

And the EAV table spreads horizontally as new attributes are created with columns labeled attribute_1 attribute_2, which contain values ​​for this particular object.

In any case - I'm trying to make the methods dynamic for the entity model, so I can call @ entity.actual_attribute_name and not @ entity.entity_attribute_value.field_5

Here is the code that I thought worked -

  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, when I returned to the code, I realized that I could set the attribute in the rails console, and it would return the correct field, when I saved the record, EntityAttributeValue was not updated (represented as self.project_category_field_value, above.)

Therefore, looking at it further, it seemed to me that I just needed to add a before_update or before_save callback to manually save the attribute, and exactly where I noticed in the callback, it launched the method_missing callback, as if the object (and the new object was a copy of the original object), or something else, I'm not quite sure. But at some point during saving or earlier, my attribute disappears into oblivion.

So, I assume that I halfway answered my question after entering it, I need to set the instance variable and check if it exists at the beginning of my method_missing method (right?) Maybe this is not what happens I don’t know, but I also ask if there is a better way to do what I'm trying to do.

And if using the method_missing method is a bad idea, please explain why, as I look through the messages about the missing method, I heard some people clapping it, but none of these people were worried, offering a reasonable explanation of why the method is missing - this is a bad decision.

Thanks in advance.

+6
source share
3 answers

Something serious programming will continue in the method_missing department. What you need is something more:

 def method_missing(name, *args) if (method_name = dynamic_attribute_method_for(name)) method_name.send(*args) else super end end 

Then you can try to break it into two parts. The first is a method that decides whether it can handle the call with the given name, here dynamic_attribute_method_for , and the second is the actual method. The job of the former is to ensure that the latter is working by the time it is called, possibly using define_method , to avoid having to repeat all this again the next time you access the same method name.

This 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 say what happens in your method, because the structure is not clear and seems to be very dependent on the context of your application.

From a design point of view, it might be easier for you to create a special wrapper class that encapsulates all these functions. Instead of calling object.attribute_name you call object.dynamic_attributes.attribute_name , where in this case dynamic_attributes is created by the request:

 def dynamic_attributes @dynamic_attributes ||= DynamicAccessor.new(self) end 

When this object is initialized, it will pre-configure itself with any methods, and you will not have to deal with this method, which is absent.

+2
source

You can see my presentation, where I described how to delegate methods to related EAV models with ActiveRecord

For example, we use STI for our Product models, and we have an Attribute for them.

First we create an 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 of 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 for our related models, and the .eav method, which generates attribute methods.

All. Now we can create our product models that are inherited from the Product class.

 class SimpleProduct < Product attr_accessible :name eav :code, :string eav :price, :float eav :quantity, :integer eav :active, :boolean end 

Using:

 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 more complex solution that allows you to create attributes at runtime or use query methods to get data, you can look at my hydra_attribute gem that implements the EAV models for active_record.

+3
source

For everyone who is trying to do the same, but with problems, the problems / solutions, as far as I could understand, were:

1) Although I thought the following code would work:

 self.project_category_field_value.send("field_#{field.id}".to_sym, *args) 

This would return a new instance of the associated model each time, so it was lost.

2) You must manually save the associated object, since the corresponding model will not be saved. I put a flag on the model and added a callback to save the associated model if the flag exists, for example.

 case(type) when :writer self.update_dynamic_attributes=(true) etc...... 

and then the callback

  before_update :update_dynamic_attributes, :if => :update_dynamic_attributes? def update_dynamic_attributes? instance_variable_get("@update_dynamic_attributes") end def update_dynamic_attributes=(val) instance_variable_set("@update_dynamic_attributes",val) end def update_dynamic_attributes self.project_category_field_value.save end 

3) Return to # 1, the biggest problem is that a new instance of the object was returned every time. I tried using the define_method method before asking this question, but it didn’t work for me, and that turned out to be what I needed to do to get it working. - The decision made me feel pretty stupid, but I'm sure that others will come across this, so make sure that if you use define_method directly in the active recording class, you call

 self.class.send(:define_method, name) 

instead

 self.send(:define_method, name) 

or you will :( when it does not work

0
source

Source: https://habr.com/ru/post/912448/


All Articles