ruby on rails使用method_missing问题从DB动态属性字段

时间:2012-04-05 01:13:53

标签: ruby-on-rails ruby activerecord attributes metaprogramming

所以,我以为我昨晚有这个工作,可以发誓。现在它没有用,我觉得有时间寻求帮助。

我在数据库中定义动态字段,半EAV风格,现在让我们说清楚我不在乎听到你对EAV是否是个好主意的看法:)

无论如何我的做法与我过去做的有点不同,基本上在添加属性(或字段)时,我创建了一个特定属性表迁移的添加列并运行它(或删除它) - - 无论如何,因为中间有一个类别层,它是定义所有属性的直接关系,我不能使用实际属性名作为列名,因为属性是特定于类别的。

所以,如果它可以帮助你想象

    Entity
    belongs_to :category

    Category
    has_many :entities

    EntityAttribute
    belongs_to :category

    EntityAttributeValue
    belongs_to :entity_attribute
    belongs_to :entity        

EAV表在创建新属性时水平跨越,其列标记为attribute_1 attribute_2,其中包含该特定实体的值。

无论如何 - 我试图让方法在实体模型上动态化,所以我可以调用@ entity.actual_attribute_name,而不是@ entity.entity_attribute_value.field_5

以下是我认为正在运行的代码 -

    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

然后今天当我回到代码时,我意识到虽然我可以在rails控制台中设置属性,但它会返回正确的字段,当我保存记录时,EntityAttributeValue没有被更新(表示为self.project_category_field_value ,上面。)

所以在进一步调查之后看起来我只需要添加一个before_update或before_save回调来手动保存属性,这就是我注意到的,在回调中,它会重新运行method_missing回调,就像对象一样被复制(并且新对象是原始对象的副本),或者其他什么,我不太确定。但是在保存过程中或之前的某个时刻,我的属性消失了。

所以,我想我在输入后解决了我自己的问题的一半,我需要设置一个实例变量并检查它是否存在于我的method_missing方法的开头(对吗?)也许这不是发生了什么我不知道,但我也在问是否有更好的方法来做我想做的事情。

如果使用method_missing是一个坏主意,请解释为什么,因为关于方法缺失的帖子我听到有人抨击它,但没有一个人打扰提供一个合理的解释为什么方法丢失是一个坏的解决方案。

提前致谢。

3 个答案:

答案 0 :(得分:3)

您可以查看我的演示文稿,其中介绍了如何将方法委派给关联模型 EAV with ActiveRecord

例如,我们对产品模型使用 STI ,我们为它们关联了属性模型。

首先,我们创建抽象的属性模型

class Attribute < ActiveRecord::Base
  self.abstract_class = true
  attr_accessible :name, :value
  belongs_to :entity, polymorphic: true, touch: true, autosave: true
end

然后我们所有的属性模型都继承自这个类。

class IntegerAttribute < Attribute
end

class StringAttribute < Attribute
end

现在我们需要描述基础产品类

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

因此,我们将#eav_attr_model方法(代理)方法添加到我们的关联模型和.eav方法中,以生成属性方法。

一切都好。现在我们可以创建继承自产品类的产品模型。

class SimpleProduct < Product
  attr_accessible :name

  eav :code, :string
  eav :price, :float
  eav :quantity, :integer
  eav :active, :boolean
end

用法:

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

如果您需要一个更复杂的解决方案,允许在运行时创建属性或使用查询方法来获取数据,您可以查看我的gem hydra_attribute,它实现 EAV < / strong>用于active_record模型。

答案 1 :(得分:2)

这是method_missing部门正在进行的一些非常激烈的编程。你应该拥有的是更像这样的东西:

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

然后,您可以尝试将其分为两部分。第一个是创建一个方法来决定它是否可以处理具有给定名称的调用,这里是dynamic_attribute_method_for,第二个是有问题的实际方法。前者的工作是确保后者在调用时起作用,可能使用define_method来避免在下次访问相同的方法名时再次完成所有这些操作。

该方法可能如下所示:

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

我不知道你的方法发生了什么,因为结构不清楚,而且它似乎高度依赖于你的应用程序的上下文。

从设计角度来看,您可能会发现创建一个封装所有这些功能的特殊用途包装类更容易。您可以拨打object.attribute_name来调用object.dynamic_attributes.attribute_name,而不是拨打dynamic_attributes,在这种情况下,def dynamic_attributes @dynamic_attributes ||= DynamicAccessor.new(self) end 是按需创建的:

{{1}}

当初始化该对象时,它将使用所需的任何方法预先配置自己,并且您不必处理此方法缺少的东西。

答案 2 :(得分:0)

对于其他任何试图做同样事情但遇到问题的人,我可以想出的问题/解决方案是:

1)虽然我认为以下代码可行:

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

每次都会返回相关模型的新实例,这就是它迷失的原因。

2)需要手动保存相关对象,因为不会保存相关模型。我最终在模型上放了一个标志,并添加了一个回调来保存相关的模型,如果存在标志,例如

case(type)

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

然后回调,

  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)回到#1,最大的问题是每次都返回新的对象实例。在问这个问题之前我尝试使用define_method方法,但它对我不起作用,最终成为我需要做的才能让它工作。 - 解决方案让我感到相当愚蠢,但我确定其他人也会遇到它,因此,请确保如果您直接在活动记录类中使用define_method,则调用

self.class.send(:define_method, name)

而不是

self.send(:define_method, name)

或者你会:(当它没有用的时候