2012-04-05 13 views
6

Entonces, pensé que tenía esto funcionando anoche, podría haberlo jurado. Ahora no funciona, y creo que es hora de pedir ayuda.ruby ​​on rails campos de atributos dinámicos de DB utilizando los problemas de method_missing

Im campos dinámicos definen en la base de datos, estilo semi EAV, y le acaba de afirmar en este momento no me importa escuchar sus opiniones sobre si EAV es una buena idea o no :)

De todas formas estoy haciendo un poco de forma diferente a como lo hice en el pasado, básicamente cuando se agrega un atributo (o campo), creo una columna de agregar a una migración de la tabla de atributos en particular y la ejecuto (o la elimino) - ANYWAYS, porque hay una capa de categoría sentado en el medio, que es la relación directa donde se definen todos los atributos, no puedo usar el nombre del atributo real como nombre de columna, ya que los atributos son específicos de la categoría.

Por lo tanto, si ayuda a visualizar

Entity 
    belongs_to :category 

    Category 
    has_many :entities 

    EntityAttribute 
    belongs_to :category 

    EntityAttributeValue 
    belongs_to :entity_attribute 
    belongs_to :entity   

y mesa de EAV se extiende horizontalmente a medida que se crean nuevos atributos, con columnas etiquetadas attribute_2 attribute_1, que contienen los valores de esa entidad en particular.

De todas formas - Estoy tratando de hacer que los métodos dinámicos en el modelo de entidad, por lo que puedo llamar @ entity.actual_attribute_name, en lugar de @ entity.entity_attribute_value.field_5

Este es el código que pensé que estaba trabajando - -

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 

Entonces hoy como volví al código, me di cuenta, aunque podía establecer el atributo en la consola de rieles, y sería devolver el campo correcto, cuando guarda el registro, la EntityAttributeValue no estaba siendo actualizado (representado como self.project_category_field_value, arriba.)

Así que después de analizarlo aún más, parecía que solo tenía que agregar una devolución de llamada before_update o before_save para guardar el atributo manualmente, y es allí donde noté, en la devolución de llamada, que volvería a ejecutar la devolución de llamada method_missing, como si el objeto fuera al estar duplicado (y el nuevo objeto era una copia del objeto original), o algo así, no estoy del todo seguro. Pero en algún momento durante el proceso de guardar o antes, mi atributo desaparece en el olvido.

Bueno, supongo que a mitad de camino respondí mi propia pregunta después de tipearla, necesito establecer una variable de instancia y verificar si existe al principio de mi método method_missing (¿no?) Tal vez eso no es lo que sucediendo, no lo sé, pero también estoy preguntando si hay una forma mejor de hacer lo que estoy tratando de hacer. Y si utilizar method_missing es una mala idea, explique por qué, al pasar por mensajes sobre el método faltante oí que algunas personas lo criticaban pero ninguna de esas personas se molestó en ofrecer una explicación razonable de por qué el método faltante era una mala solución .

Gracias de antemano.

Respuesta

2

Esa es una programación muy seria que está sucediendo en el departamento method_missing. Lo que debe tener es algo más parecido a esto:

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

A continuación, puede tratar de descomponerlo en dos partes. El primero es crear un método que decida si puede manejar una llamada con un nombre dado, aquí dynamic_attribute_method_for, y el segundo es el método real en cuestión.El trabajo del primero es garantizar que el último funcione cuando se lo llama, posiblemente usando define_method para evitar tener que pasar por todo esto la próxima vez que acceda al mismo nombre de método.

Ese método podría tener este aspecto:

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 

No puedo decir lo que está pasando en su método como la estructura no es clara y parece depender del contexto de su aplicación altamente.

Desde una perspectiva de diseño, es posible que resulte más fácil crear una clase contenedora de propósito especial que englobe toda esta funcionalidad. En lugar de llamar object.attribute_name que se dice object.dynamic_attributes.attribute_name donde en este caso se crea dynamic_attributes en la demanda:

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

Cuando se inicializa ese objeto se pre-configurarse con cualquier método que se requieren y no tendrá que hacer frente con este método falta algo.

+0

gracias por la respuesta, no pude hacerlo funcionar solo por el código anterior pero utilicé parte de él en la refactorización, así que acepté como respuesta – thrice801

0

Para cualquier otra persona tratando de hacer mismo tipo de cosas, pero que tienen problemas, los problemas/soluciones Por lo que pude averiguar eran:

1) Aunque pensé que el siguiente código funcionaría:

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

Eso devolvería una nueva instancia del modelo relacionado cada vez, por lo que se estaba perdiendo.

2) Es necesario guardar manualmente el objeto relacionado, ya que el modelo relacionado no se guardará. Terminé poniendo una bandera en el modelo y añadiendo una devolución de llamada para guardar el modelo relacionado si existía una bandera, p.

case(type) 

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

y después de devolución de llamada,

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) Volver a # 1, el mayor problema era nueva instancia de objeto se está devolviendo cada vez. Traté de usar el método definir_metodo antes de hacer esta pregunta, pero no funcionaba para mí, y eso terminó siendo lo que tenía que hacer para que funcionara. - La solución me hizo sentir bastante estúpida, pero estoy seguro que otros se ejecutará en ella, así, así, asegúrese de que si está utilizando define_method directamente dentro de la clase registro activo, se llama a

self.class.send(:define_method, name) 

en lugar de

self.send(:define_method, name) 

o si será :(cuando hay worky

3

usted puede mirar en mi presentación en la que he descrito cómo delegar métodos asociados a los modelos EAV with ActiveRecord

Por ejemplo, utilizamos STI para nuestros modelos Producto y tenemos asociado Modelos de atributo para ellos.

En primer lugar debemos crear el modelo abstracto

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

Atributo Entonces todos nuestros modelos de atributos se heredan de esta clase.

class IntegerAttribute < Attribute 
end 

class StringAttribute < Attribute 
end 

Ahora tenemos que describir la base de clase producto

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 

Así que añadimos el método #eav_attr_model que es un proxy de método para nuestros modelos asociados y el método .eav que genera métodos de atributos .

Es todo. Ahora podemos crear nuestros modelos de productos que se heredan de Producto class.

class SimpleProduct < Product 
    attr_accessible :name 

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

Uso:

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 

si necesita aa solución más compleja que permite crear atributos en tiempo de ejecución o utilizar métodos de consulta para obtener datos se puede ver en mi joya hydra_attribute que implementa el EAV para modelos active_record.

+0

Gracias por compartir. Solo estoy tratando de obtener un modelo de EAV, y esto ha sido útil. –