Mejorando el control sobre los callbacks de ActiveRecord
ActiveRecord, la librería encargada en Rails de gestionar toda la capa de modelo de datos, incorpora de serie unos callbacks que permiten controlar el ciclo de actualización de un objeto de manera más o menos precisa.
En el siguiente gráfico, extraído del Agile Web Development ed. 2, se puede ver la jerarquía de callbacks y el orden en el que se ejecutan:

Su uso es muy recomendable si queremos escribir un código limpio en los modelos de nuestra aplicación.
Sin embargo, hace unas semanas nos ha surgido la necesidad de ir un poco más allá en los callbacks. Os cuento la situación:
Imaginad un modelo Post y un modelo Blog. En el modelo Blog, tengo un atributo que me dice cuántos posts publicados tiene ese blog (un atributo desnormalizado). Pues bien, con un sencillo after_save en el modelo Post puedo conseguir mantener actualizado dicho campo sin mucho esfuerzo:
class Post < ActiveRecord::Base
...
def after_save
blog.posts_count = Post.count(:conditions => ["blog_id = ? and status = ?", id, STATUS_PUBLISH])
blog.save
end
...
end
Este callback se ejecutará, efectivamente, cada vez que publique un post. Sin embargo, también lo hará cada vez que modifique un post que ya existe, para editar el contenido del mismo, o su título, atributos que no están relacionados con el blog al que pertenece el post.
Es decir, en realidad yo sólo quiero que se ejecute mi método after_save en un caso concreto: cuando se publica un post. O mejor dicho: cuando un post cambia de estado. El estado, o status, de un post simplemente indica si es un borrador o está publicado: si está en borrador, no aparece en los listados públicos y si está publicado sí. Por eso, el contador de posts del blog sólo se refiere a posts publicados.
Resumiendo: sólo quiero actualizar el blog asociado a un post cuando cambie el atributo status del mismo.
Y esto, amigos míos, ActiveRecord no lo permite, o por lo menos no de forma nativa. Pero con un poco de imaginación se puede llegar a saber dentro de un callback qué atributos se han modificado en el objeto.
Para ello, utilizaremos una variable de instancia (un atributo, vaya) al que llamaremos before_update_attributes y que va a hacer referencia a los atributos del objeto antes de la actualización. Lo declararemos y lo utilizaremos así:
attr_reader :before_update_attributes
...
def before_validation
...
@before_update_attributes = Post.find(id).attributes if id
end
Es decir, en el callback before_validation, el primer callback de los que se ejecutan al modificar un objeto, vamos a guardar los valores de los atributos del objeto en nuestra variable para, posteriormente cuando nos haga falta, comparar los valores actuales con los que había previamente, antes de modificarlo.
Así, nuestro after_save podría quedar así:
def after_save
# comprobamos que exista el before_update_attributes y que contenga el status
return if before_update_attributes and not attributes.diff(before_update_attributes).keys.include?("status")
# si no existe es que el objeto es nuevo
# si no contiene el status, es que este atributo no se ha modificado
blog.posts_count = Post.count(:conditions => ["blog_id = ? and status = ?", id, STATUS_PUBLISH])
blog.save
end
Y con este código, realizar los tests es realmente sencillo y no requiere realizar ninguna comprobación extraña, simplemente actualizar o no el status y verificar que se actualiza o no el atributo posts_count del blog.
Reconozco que no es la forma más elegante y que se podría mejorar e incluso generalizar de alguna manera (¿alguien se anima a hacer un plugin?), pero como primera aproximación parida en el tren camino a Valencia no está mal.
Así que animáos a opinar y, sobretodo, me interesa saber si conocéis alguna manera de abordar este problema de otra forma diferente.
