Actualmente estoy trabajando en un módulo de una aplicación web cuyo objetivo es presentar reportes de accesos a un predio, la aplicación está desarrollada en php y postgresql, los reportes están basados en vistas, dónde tengo una "vista base" llamada "stats_ingresos", y las demás vistas hacen uso de "stats_ingresos". Todo muy lindo, pero actualmente este sistema es muy lento, las consultas a estas vistas demoran más 2 minutos lo cual es inaceptable para una aplicación web, entonces decidí pasar esta vista base a tabla, con lo cual el costo de la consulta se reduce a 0. Obviamente esta solución tiene una desvetaja, tengo que actualizar la tabla "stats_ingresos" perdiódicamente, la actualización se va a realizar una vez por día y se actualizarán todos los accesos del mes (ya sé que esto se puede mejorar). Para ir al grano, he decidido implentar este script de update con ruby + active_record y como metodología bdd usando rspec. Esta es la tabla que va a reemplazar a la "vista base":
CREATE TABLE stats_ingresos ( id SERIAL NOT NULL, nombre VARCHAR(25), apellido VARCHAR(25), dni VARCHAR(12), id_institucion INTEGER, institucion VARCHAR(50), dia SMALLINT, mes SMALLINT, anio SMALLINT, ts timestamp, PRIMARY KEY (id) );
Entonces, resumiendo, la idea es reemplazar la vista "stats_ingresos" por una tabla exactamente igual (con los mismos campos), para que no tenga "efectos secundarios" en el código existente, y actualizar esta tabla todos los días. Para esto, voy a necesitar 2 clases una para la tabla "access" y otra para la nueva tabla "stats_ingresos" (la consulta que genera stats_ingresos está basada en la tabla "access")
require 'rubygems' gem 'activerecord', '>= 1.15.2' require 'active_record' require 'yaml'class Access < ActiveRecord::Base set_table_name "access" set_primary_key "access_id"
ActiveRecord::Base.pluralize_table_names = false Sql = "SELECT per.apellido, per.nombre, acc.pin as dni, soc.descripcion as institucion, soc.id_institucion, date_part('day'::text, acc.access_date) AS dia, date_part('month'::text, acc.access_date) AS mes, date_part('year'::text, acc.access_date) AS anio, acc.access_date AS ts FROM access as acc JOIN persona as per ON acc.pin::numeric = per.numero_documento JOIN socio_institucion as soc ON acc.pin::numeric = soc.numero_documento WHERE acc.pin != '------------' "
def Access.stats_ingresos_view date_cond = '' Access.find_by_sql("#{Sql}" + date_cond) end
def Access.sql_si_view hash_date @cond = " and date_part('year'::text, acc.access_date) = " \ "#{hash_date['anio']}" if not hash_date['anio'].nil? @cond = @cond + " and date_part('month'::text, acc.access_date) = " \ + "#{hash_date['mes']}" if not hash_date['mes'].nil? @cond end
def Access.method_missing(method_name, *args) if method_name.to_s =~ /^stats_ingresos_view/ hash_date = Hash.new hash_date["#{Access.por(method_name.to_s)[0]}"] = args[0] hash_date["#{Access.por(method_name.to_s).pop}"]= args.pop Access.stats_ingresos_view( sql_si_view(hash_date) ) end end
def Access.por str_por s = str_por.split('_por_').last s.split('_y_') end end
El método más importante en la clase anterior es Access.stats_ingresos_view, este hace la consulta en la tabla access y retorna un hash con los registros resultantes, este método toma como parámetro un string con las condiciones de búsqueda por fecha (año y mes), a su vez el método Access.sql_si_view toma como argumento hash con los valores de anio y mes y arma el string necesario para Access.stats_ingresos_view. Por otro lado redefino Access.method_missing para poder llamar a Access.stats_ingresos_view_por_anio_y_mes(2007, 10) y Access.stats_ingresos_view_por_anio(2006) por ejemplo. Estos son los specs para Access:
require '../models/access'#FIXME -> ver el tema de flexmock
# ------------------ Conexión FIXME -------------------------------------
@config = YAML::load(File.open("../../config/database.yml"))["development"] ActiveRecord::Base.establish_connection( @config )
# -----------------------------------------------------------------------
context "Metodo 'por'" do
specify "Un string con anio y mes" do @arr_con = Access.por 'un_metodo_por_anio_y_mes' @arr_con.should == ['anio', 'mes'] end
specify "Un String con anio solo" do @arr_con = Access.por 'un_metodo_por_anio' @arr_con.should == ['anio'] end
end
context "Busco los ingresos por año" do
setup do `../../test/fixtures/delete_all.sh laptop` `../../test/fixtures/load_all.sh laptop` end
specify "Armo la condición con el anio 2007" do hash_anio_mes = {'anio' => '2007'} @cond = Access.sql_si_view hash_anio_mes @cond.should == " and date_part('year'::text, acc.access_date) = 2007" end
specify "Armo la condición con el anio 2006 y mes 11" do hash_anio_mes = {'anio' => '2006','mes' => '11'} @cond = Access.sql_si_view hash_anio_mes @cond.should == " and date_part('year'::text, acc.access_date) = " + \ "2006 and date_part('month'::text, acc.access_date) = 11" end
specify "Para el año 2007" do hash_anio_mes = {'anio' => '2007'} @accesos_view = Access.stats_ingresos_view( \ Access.sql_si_view(hash_anio_mes)).first @accesos_view.anio.should == '2007' end
specify "Para el año 2006 y mes 11" do hash_anio_mes = {'anio' => '2006','mes' => '10'} @accesos_view = Access.stats_ingresos_view( \ Access.sql_si_view(hash_anio_mes)).first @accesos_view.anio.should == '2006' @accesos_view.mes.should == '10' end
end
context "Busco los ingresos a la ruby-way" do
setup do `../../test/fixtures/delete_all.sh laptop` `../../test/fixtures/load_all.sh laptop` end
specify "Para el año 2007" do @accesos_view = Access.stats_ingresos_view_por_anio('2007').first @accesos_view.anio.should == '2007' end
specify "Para el año 2006 y mes 10" do @accesos_view = Access.stats_ingresos_view_por_anio_y_mes( \ '2006', '10').first @accesos_view.anio.should == '2006' @accesos_view.mes.should == '10'
end
specify "Para el mes 10 y año 2006" do @accesos_view = Access.stats_ingresos_view_por_mes_y_anio( \ '10', '2006').first @accesos_view.anio.should == '2006' @accesos_view.mes.should == '10' end
specify "Llamo a un método inexistente" do @no_existe = Access.esto_no_existe_ingresos_view_por_mes("10") @no_existe.should be_nil end
specify "Busco todos los ingresos" do @si_view = Access.stats_ingresos_view @si_view.length.should == 16 end
end
Esta es la clase para la nueva tabla:
require 'rubygems' gem 'activerecord', '>= 1.15.2' require 'active_record' require 'yaml'class StatsIngresos < ActiveRecord::Base set_table_name "stats_ingresos"
ActiveRecord::Base.pluralize_table_names = false
def StatsIngresos.update_view arr_view @insert_count = 0
arr_view.each do |@ing| @stat_ingresos = StatsIngresos.new @insert_count = @insert_count + 1 \ if @stat_ingresos.update_attributes @ing.attributes end
@insert_count end
end
Esto todo por ahora, se aceptan sugerencias y comentarios, continuará....