En este post voy a mostrar un ejemplo de como implementar versionado de una tabla con Ruby On Rails.
El problema que estamos atacando es el siguiente: tenemos un ABM sobre una tabla y queremos que cada vez que se realice una modificación sobre algún registro, la versión anterior a la modificada quede guardada y podamos en caso necesario revertir nuestros cambios y volver a esa versión del registro (o a cualquier version anterior).
Para eso vamos a utilizar un plugin de Rails llamado acts_as_versioned
Empezamos creando la aplicacion rails
rails versiones
cd versiones
A continuación instalamos el plugin. Para eso primero actualizamos la lista de plugins disponibles
ruby script/plugin discover ruby
Y después instalamos el plugin que nos interesa
script/plugin install acts_as_versioned
El siguiente paso es la configuración de la base de datos. No vamos a usar nada específico de ningún motor de base de datos, asi que no voy a explicar en detalle como hacerlo. Aquí hay una explicación detallada de instalación y configuración con MySQL
A continuación generamos el modelo para nuestro ejemplo de versionado, que va a ser una tabla de clientes
ruby script/generate model Cliente
Esto nos genera una serie de archivos, entre los cuales esta el archivo de migración que utilizaremos para crear nuestra tabla . Completamos este script de migracion con la estructura de nuestra tabla de clientes:
db/migrate/001_create_clientes.rb
class CreateClientes < ActiveRecord::Migration
def self.up
create_table :clientes do t
t.column :nombre,:string
t.column :apellido,:string
t.column :direccion,:string
end
end
def self.down
drop_table :clientes
end
end
Ejecutamos la migración
rake db:migrate
Y generamos el controller y las views necesarias para la edicion de nuestra tabla
script/generate controller cliente list edit
Completamos el controller y las views que generamos con el siguiente código, para tener la posibilidad de ver y editar los registros de nuestra tabla (no voy a entrar en explicaciones con esta parte porque no tiene que ver directamente con el tema de versionado y además está abundantemente documentado)
app/controller/cliente_controller.rb
class ClienteController < ApplicationController
def list
@clientes = Cliente.find(:all)
end
def edit
@cliente = Cliente.find(params[:id])
end
def change
@cliente = Cliente.find(params[:id])
if(@cliente.update_attributes(params[:cliente])) then
redirect_to(:action => 'list')
else
render(:action => 'edit')
end
end
end
app/view/cliente/list.rhtml
<Table>
<% @clientes.each do cliente %>
<TR>
<TD> <%= cliente.nombre%> </TD>
<TD> <%= cliente.apellido%> </TD>
<TD> <%= cliente.direccion%> </TD>
<TD> <%= link_to 'Edicion', {:action => 'edit',:id => cliente}%> </TD>
</TR>
<%end%>
</Table>
app/views/cliente/edit.rhtml
<%= start_form_tag({:action => 'change', :id => @cliente}, :id => "form") %>
<%= error_messages_for 'cliente' %>
<p><label for="cliente_nombre">Nombre:</label><br/>
<%= text_field 'cliente', 'nombre' %></p>
<p><label for="cliente_apellido">Apellido:</label><br/>
<%= text_field 'cliente', 'apellido' %></p>
<p><label for="cliente_direccion">Direccion:</label><br/>
<%= text_field 'cliente', 'direccion' %></p>
<%= submit_tag "Salvar Cambios" %>
<%= end_form_tag %>
Con esto tenemos funcional una aplicación que nos permite listar y editar nuestros clientes. Vamos a generar algunos registros con una migracion para poder probarla:
ruby script/generate migration crea_clientes_prueba
class CreaClientesPrueba < ActiveRecord::Migration
def self.up
(1..20).each do |index|
Cliente.new(:nombre => sprintf("Nombre_%d",index),
:apellido => sprintf("Apellido_%d",index),
:direccion => sprintf("Direccion_%d",index)).save!
end
end
def self.down
end
end
rake db:migrate
Levantamos el server:
ruby script/server
y, apuntando un browser a http://localhost:3000/cliente/list , debería aparecer el listado de clientes.
Ahora vamos a modificar este ABM para que maneje versiones. En primer lugar indicaremos que nuestro modelo es versionado.
app/model/cliente.rb
class Cliente < ActiveRecord::Base
acts_as_versioned
end
Generamos la tabla en la que se guardaran las diferentes versiones, nuevamente con una migración.
script/generate migration crea_tabla_versiones
Completamos el contenido del archivo de migracion
class CreaTablaVersiones < ActiveRecord::Migration
def self.up
Cliente.create_versioned_table
end
def self.down
Cliente.drop_versioned_table
end
end
Los métodos create_versioned_table y drop_versioned_table se agregan automáticamente al modelo cuando declaramos que este es versionado (cuando decimos acts_as_versioned) que generan y borran la tabla auxiliar necesaria para guardar las versiones de los registros de la tabla principal
Ejecutamos la migración para crear la tabla auxiliar
rake db:migrate
Y ya está, nuestros clientes tienen múltiples versiones. Para ver como funciona esto y algunos ejemplos, vamos a recurrir a la consola de rails (que es un excelente recurso cuando queremos jugar con nuestro modelo sin escribir todavía la interfaz gráfica).
ruby script/console
#Creamos un cliente y lo salvamos
>> cliente = Cliente.new(:nombre => "Homero",
:apellido => "Simpson",
:direccion => "Avenida Siempreviva 742, Springfield")
>> cliente.save!
#En que version estamos?
>> cliente.version
=> 1
#Ahora cambiamos la direccion y salvamos los cambios:
>> cliente.direccion = "430 Camino Spalding"
=> "430 Camino Spalding"
>> cliente.save!
=> true
#En que version estamos?
>> cliente.version
=> 2
#Accedemos a los datos de las distintas versiones
>> cliente.versions[0].direccion
=> "Avenida Siempreviva 742, Springfield"
>> cliente.versions[1].direccion
=> "430 Camino Spalding"
#Y deshacemos los cambios para volver a la versión anterior
>> cliente.direccion
=> "430 Camino Spalding"
>> cliente.revert_to(1)
=> true
>> cliente.direccion
=> "Avenida Siempreviva 742, Springfield"
>> cliente.save!
=> true
#Es interesante ver que el hecho de volver a la version anterior no eliminó la versión intermedia
>> cliente.version
=> 3
>> cliente.versions[0].direccion
=> "Avenida Siempreviva 742, Springfield"
>> cliente.versions[1].direccion
=> "430 Camino Spalding"
>> cliente.versions[2].direccion
=> "Avenida Siempreviva 742, Springfield"
Ahora que ya probamos esto desde la consola (y no crean que las direcciones son al azar) ,vamos a agregar la información a la interfaz gráfica. En primer lugar vamos a agregar al listado de clientes el numero de version y una opcion que nos permita volver a una version anterior.
<Table>
<% @clientes.each do cliente %>
<TR>
<TD> <%= cliente.nombre%> </TD>
<TD> <%= cliente.apellido%> </TD>
<TD> <%= cliente.direccion%> </TD>
<TD> <%= cliente.version%> </TD>
<TD> <%= link_to 'Versiones', {:action => 'versiones',:id => cliente}%> </TD>
<TD> <%= link_to 'Edicion', {:action => 'edit',:id => cliente}%> </TD>
</TR>
<%end%>
</Table>
Agregamos en el controller el codigo para obtener las versiones anteriores:
def versiones
@cliente = Cliente.find(params[:id])
@versiones_cliente = @cliente.versions
end
Y creamos una view versiones.rhtml que nos permita ver las versiones anteriores:
<Table>
<% @versiones_cliente.each_with_index do version,index %>
<TR>
<TD> <%= version.nombre%> </TD>
<TD> <%= version.apellido%> </TD>
<TD> <%= version.direccion%> </TD>
<TD> <%= link_to 'Volver a', {:action => 'volver_a_version',:id =>@cliente,:version => index+1}%> </TD>
</TR>
<%end%>
</Table>
Con estos agregados podemos ver facilmente las versiones anteriores de un cliente.
Ahora nos falta implementar la vuelta a una versión anterior. Como se puede ver, en el listado de versiones ya incluimos un link que nos permite volver a cada version. Solo nos falta incluir en el controller la acción correspondiente a esa vuelta atrás:
def volver_a_version
@cliente = Cliente.find(params[:id])
@version = params[:version]
@cliente.revert_to(@version.to_i)
@cliente.save!
redirect_to(:action => 'list')
end