Jun 20 Evitando dejar huerfanos: plugin de Rails

tags: rails plugin metaprogramacion has_many | comments

El problema: has_many no protege a los padres

¿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}. La primera borra los hijos llamando su metodo destroy, la segunda los borra sin llamar el destroy, y la tercera les pone el puntero a null a los hijos sin llamar sus callbacks save. Pero nos falta el caso en que no queremos borrar el registro si tiene hijos.

La solucion: plugear las Asociaciones de ActiveRecord

El plugin stop_deletion_if_has_children resuelve esto de manera sencilla con un poco de metaprogramación:

La declaración de la restricción al borrar queda así en el modelo padre:

  1. 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:

  1. 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 declarar las reglas de infleccion para español, o 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

Y aqui tenemos 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).
blog comments powered by Disqus