Stop_deletion_if_has_children: metaprogramación en Rails

Posted by joahking Mon, 15 Oct 2007 02:34:00 GMT

¿Has intentado definir en una asociación que se detenga el borrado del objeto si existen otros que lo refieran? Por ejemplo: Padre, tiene Hijos y Nietos, y no quieres que sea destruido si tiene algún hijo o nieto. La asociación has_many de Rails provee tres opciones para mantener la integridad de datos con :dependent => {:destroy | :delete_all | nullify}. Pero pasa por alto este caso tan común.

Por suerte Rails brinda posibilidades aun en el caso de que algo falte, y esta es una buena oportunidad para un plugin y usar un poco de metaprogramación. El plugin stop_deletion_if_has_children resuelve esto de manera sencilla:

La declaración de la restricción en el modelo es así:


# padre.rb
has many :hijos
has_many :nietos
stop_deletion_if_has_children :hijo, :nieto

(Nótese que el singular es importante en la declaración de cada parámetro pasado al plugin, según la implementación dada.)


# init.rb
require 'stop_deletion_if_has_children'
ActiveRecord::Base.send(:extend,Qvitta::StopDeletionIfHasChildren)

El plugin recorrerá la lista de objetos enlazados pasados como parámetros y chequeará que no existan records en el momento de destruir el objeto.

Esto es lo que hace:

# stop_deletion_if_has_children.rb
module Qvitta #:nodoc:
  module StopDeletionIfHasChildren #:nodoc:
    def stop_deletion_if_has_children(*children)
      define_method "children_check" do
        ret = true
        for child in children do
          if self.send("#{child}".to_s.pluralize).length > 0
            self.errors.add_to_base "Error: #{self.class} contiene #{child.to_s.camelize.pluralize}" 
            ret = false
          end
        end
        return ret
      end
      before_destroy :children_check
    end
  end
end

Las claves en el código anterior son:
  • *children: el asterisco le dice a Rails que el parámetro del método es un arreglo de valores.
  • before_destroy :children_check: que interpone el método children_check justo antes de la destrucción del objeto, y permite la destrucción return true o la detiene return false (se devuelven todos los mensajes de errores posibles en una sola pasada).
La metaprogramación viene dada por las lineas:
  • define_method "children_check" do: define al vuelo el método children_check.

Si como yo, tienes modelos con nombres en español la linea corta:

if self.send("#{child}".to_s.pluralize).length > 0

puede fallar pues pluralize lo hará en ingles. En ese caso necesitas hacer el chequeo desde los descendientes hacia el padre con esta más larga:

if "#{child}".to_s.camelize.constantize.send("find_all_by_#{self.class.to_s.downcase}_id",self.id).length > 0

Esta linea más larga nos da el pie didáctico para más de metaprogramación:
  • camelize: para convertir la cadena ‘hijo’ en ‘Hijo’.
  • contantize: que convierte la cadena ‘Hijo’ en la clase Hijo.
  • send: que invoca el método pasado como parámetro en la clase obtenida con el tratamiento anterior (en nuestro caso: Hijo.find_all_by_padre_id).

Pues eso es. A ver si con un poco de suerte (y tiempo) ponemos el repositorio de plugins de Qvitta accesible esta semana, y asi se podrá descargar.

Posted in  | Tags , , , ,  | no comments