12

En mi aplicación, tengo una clase llamada Presupuesto. El presupuesto puede ser de muchos tipos ... Por ejemplo, digamos que hay dos presupuestos: FlatRateBudget y HourlyRateBudget. Ambos heredan de la clase Presupuesto.¿Cómo ejecutar validaciones de subclase en herencia de tabla única?

Esto es lo que me pasa hasta el momento:

class Budget < ActiveRecord::Base 
    validates_presence_of :price 
end 

class FlatRateBudget < Budget 
end 

class HourlyRateBudget < Budget 
    validates_presence_of :quantity 
end 

En la consola, si lo hago:

b = HourlyRateBudget.new(:price => 10) 
b.valid? 
=> false 
b.errors.full_messages 
=> ["Quantity can't be blank"] 

Como, espera.

El problema es que el campo "Tipo", sobre ITS, proviene de params .. Así que tengo que hacer algo como:

b = Budget.new(:type => "HourlyRateBudget", :price => 10) 
b.valid? 
=> true 

Lo que significa que los carriles se está ejecutando validaciones en el super-clase en vez de instanciar la sub clase después de configurar el tipo.

Sé que es el comportamiento esperado, ya que estoy instanciando una clase que no necesita el campo de cantidad, pero me pregunto si de todos modos hay que indicarle a los rieles que ejecuten las validaciones para la subclase en lugar del super.

+0

Al usar STI, evitaría crear instancias de objetos de la superclase y solo funciona con las clases base, y creo que Rails utilizará las validaciones apropiadas de la superclase y las exclusivas de la subclase. – firecape

Respuesta

9

Probablemente pueda resolver esto con un validador personalizado, similar a la respuesta a esta pregunta: Two models, one STI and a Validation Sin embargo, si puede simplemente crear una instancia del subtipo deseado para comenzar, evitará la necesidad de un validador personalizado en conjunto en este caso.

Como habrás notado, establecer el campo tipo solo no cambia mágicamente una instancia de un tipo a otro. Si bien ActiveRecord usará el campo type para instanciar la clase adecuada en leyendo el objeto de la base de datos, hacerlo al revés (instanciar la superclase, luego cambiar el campo de tipo manualmente) no tiene el efecto de cambiar el objeto escriba mientras su aplicación se está ejecutando, simplemente no funciona de esa manera.

El método de validación de encargo, por otro lado, podría comprobar el campo type independientemente, una instancia de una copia del tipo apropiado (basado en el valor del campo type), y luego ejecutar .valid? en ese objeto, dando como resultado la validaciones en la subclase que se ejecuta de una manera que parece ser dinámica, a pesar de que en realidad está creando una instancia de la subclase apropiada en el proceso.

+0

Utilicé la última sugerencia de párrafo y funcionó. – robertokl

2

En lugar de establecer el tipo de teléfono directamente en el tipo como ese ... En su lugar, intente:

new_type = params.fetch(:type) 
class_type = case new_type 
    when "HourlyRateBudget" 
    HourlyRateBudget 
    when "FlatRateBudget" 
    FlatRateBudget 
    else 
    raise StandardError.new "unknown budget type: #{new_type}" 
end 
class_type.new(:price => 10) 

Incluso se puede transformar la cadena en su clase por: new_type.classify.constantize pero si viene desde params, eso parece un poco peligroso.

Si haces esto, obtendrás una clase de HourlyRateBudget, de lo contrario solo será un presupuesto.

+0

Eso es un camino por recorrer, pero quiero usar la magia accept_nested_attributes para mantener mi controlador lo más delgado posible. – robertokl

+0

Aún puedes hacer eso. El truco es conseguir que tu tipo de cuerda sea una clase. –

3

Para el que busque código de ejemplo, así es como he implementado la primera respuesta:

validate :subclass_validations 

def subclass_validations 
    # Typecast into subclass to check those validations 
    if self.class.descends_from_active_record? 
    subclass = self.becomes(self.type.classify.constantize) 
    self.errors.add(:base, "subclass validations are failing.") unless subclass.valid? 
    end 
end 
+0

Produce este error para mí: NoMethodError (método indefinido 'type 'para – Antzi

+1

¿Su modelo tiene un campo" tipo "? – Bryce

0

Mejor aún, el uso type.constantize.new("10"), sin embargo, esto depende de que el tipo de params debe ser cadena correcta idéntica a HourlyRateBudget.class.to_s

+0

Esta es la respuesta que iba a recomendar. Sin embargo, desea incluir en la lista blanca lo que es aceptable para que alguien no lo haga 't pasan en un 'type' malicioso en un' before_action' que podría hacer algo como:. estado 'render:: prohibido a menos type.constantize.in?([HourlyRateBudget, FlatRateBudget])' – mackshkatz

6

He hecho algo similar.

adaptándolo a su problema:

class Budget < ActiveRecord::Base 

    validates_presence_of :price 
    validates_presence_of :quantity, if: :hourly_rate? 

    def hourly_rate? 
     self.class.name == 'HourlyRateBudget' 
    end 

end 
+0

Esto no es t recibir votos al alza ... pero a mí me parece (como novato de los rieles) una solución muy ferroviaria. ¿Funciona esto y sigue siendo la forma en que aconsejarías resolver esto? –

+0

¡Gran respuesta simple! Puedes usar 'self .type == 'HourlyRateBudget'' también – hackhowtofaq

0

también requiere la misma y con la ayuda de la respuesta de Bryce Hice esto:

class ActiveRecord::Base 
    validate :subclass_validations, :if => Proc.new{ is_sti_supported_table? } 

    def is_sti_supported_table? 
    self.class.columns_hash.include? (self.class.inheritance_column) 
    end 

    def subclass_validations 
     subclass = self.class.send(:compute_type, self.type) 
     unless subclass == self.class 
     subclass_obj= self.becomes(subclass) 
     self.errors.add(:base, subclass_obj.errors.full_messages.join(', ')) unless subclass_obj.valid? 
     end 
    end 
end 
Cuestiones relacionadas