Jun 20 Evitando dejar huerfanos: plugin de Rails
tags:
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:
- 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ónreturn trueo la detienereturn 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).