6

所以,以为我昨晚有这个工作,可以发誓。现在它没有工作,我想是时候寻求帮助了。

我在数据库中定义动态字段,半 EAV 风格,现在让我们说一下,我不想听听你对 EAV 是否​​是个好主意的意见:)

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

所以,如果它可以帮助你形象化

    Entity
    belongs_to :category

    Category
    has_many :entities

    EntityAttribute
    belongs_to :category

    EntityAttributeValue
    belongs_to :entity_attribute
    belongs_to :entity        

随着新属性的创建,EAV 表水平跨越,列标记为属性_1 属性_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 是一个坏主意,请解释原因,因为在浏览有关方法缺失的帖子时,我听到一些人抨击它,但没有一个人愿意提供一个合理的解释来解释为什么方法缺失是一个糟糕的解决方案。

提前致谢。

4

3 回答 3

3

您可以查看我的演示文稿,其中我描述了如何 使用 ActiveRecord 将方法委托给关联模型 EAV

例如,我们将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生成属性方法的方法中。

就是这样。现在我们可以创建继承自Product类的产品模型。

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,它为 active_record 模型实现了EAV

于 2012-08-01T09:03:39.810 回答
2

method_missing这是该部门正在进行的一些非常紧张的计划。你应该拥有的更像是这样的:

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

然后,您可以尝试将其分解为两部分。第一个是创建一个方法,该方法决定它是否可以处理具有给定名称的调用,here 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_namewhere 在这种情况下dynamic_attributes按需创建:

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

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

于 2012-04-05T02:57:16.267 回答
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)

或者你会:(当它不起作用时

于 2012-04-07T00:02:10.747 回答