active_record ( Ruby ) sin Rails – Parte I

by Gastón Ramos

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á….