ActiveRecord: trucos con campos nil en las condiciones

Posted by joahking
on Aug 19, 08

ActiveRecord y olores a SQL

Digamos que tienes un objeto que referencia a otro, Hoja que tiene un arbol_id, si quieres contar las hojas caidas te puedes llevar una sorpresa:
>> Hoja.count(:conditions => ["arbol_id = ?", nil])
=> 0
Miremos en el log la query resultante:
SQL (0.000286) SELECT count(*) AS count_all FROM `hojas` WHERE (arbol_id = NULL) 
El problema es que la comparacion SQL para el valor NULL debiera ser IS y no =. Si pruebas de esta otra manera obtendras el resultado puntual deseado:
>> Hoja.count(:conditions => ["arbol_id IS ?", nil])
=> 272
# y la query correcta:
SQL (0.000286) SELECT count(*) AS count_all FROM `hojas` WHERE (arbol_id IS NULL) 

Pero el ActiveRecord adapter te lanzara una exception si en vez del nil pasas un numero como parametro. Este problema ocurre tambien usando :conditions con los find

Solucion

Si no aparece alguna mejor solucion, nos queda el workaround del find_all_by_xxx con length:
>> Hoja.find_all_by_arbol_id(nil).length
=> 272
# y la query queda bien:
Hoja Load (0.000554)   SELECT * FROM "hojas" WHERE ("hojas"."arbol_id" IS NULL)

# find_all_by... es mi heroe (menos performantico que un count, pero mi heroe):
>> Hoja.find_all_by_arbol_id(1).length
=> 300
# y otra query bien construida:
Hoja Load (0.000554)   SELECT * FROM "hojas" WHERE ("hojas"."arbol_id" = 1)¡

La raiz de los males, OOP y Datamapper

En el tratamiento de las :conditions del ActiveRecord se filtran hasta los objetos olores a SQL, lo cual no es muy Object Oriented. Miremoslo en Datamapper:

# wow! que sintaxis mas DRY (49 characteres AR vs 28 DM)
>> Hoja.count(:arbol_id => nil)
=> 272

# y estamos mas separados del SQL:
>> Hoja.count(:arbol_id => 1)
=> 300

# incluso podemos hacer comparaciones concisas (en este caso greater than):
>> Hoja.count(:arbol_id.gt => 1)
=> 435

Datamapper hace muy buen trabajo manteniendonos a salvo en tierra OOP.

Mongrel: Un fantasma en mis puertos

Posted by joahking
on Aug 13, 08
Algunas veces el mongrel no levanta porque el puerto esta ocupado aun cuando hayas matado los procesos. El problema parece ser que el Ruby se deja a veces algunos procesos vivos en algunos ambientes de desarrollo. Lo notas por un mensaje como este:
/usr/local/lib/ruby/gems/1.8/gems/mongrel-1.1.1/lib/mongrel/tcphack.rb:12:in `initialize_without_backlog': Address already in use - bind(2) (Errno::EADDRINUSE)
        from /usr/local/lib/ruby/gems/1.8/gems/mongrel-1.1.1/lib/mongrel/tcphack.rb:12:in `initialize'
por suerte contamos en Linux con mas de una manera de encontrar ese fantasma. Por ejemplo este te muestra los PIDs de procesos con archivos abiertos cuya direccion de internet machea la de la opcion -i:
$ lsof -t -i TCP:4000
6849
con el pid puedes pedir mas info sobre el proceso con ps:
$ ps 6849
  PID TTY      STAT   TIME COMMAND
 6849 pts/0    Tl     0:01 /usr/bin/ruby1.8 /usr/bin/merb
y vemos que es un merb que se ha quedado frito. Pero como?, si lo hemos matado, he aqui al ofender:
$ netstat -nap | grep 4000
(Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.)
tcp        0      0 0.0.0.0:4000            0.0.0.0:*               LISTEN      6849/ruby1.8
tambien el mas comun ps aux pero al que algunos procesos se le escapan:
$ ps aux | grep 4000
(Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.)
tcp        0      0 0.0.0.0:4000            0.0.0.0:*               LISTEN      6849/ruby1.8
y claro, “who you gonna call? Ghostsbusters!” por process id o por nombre:
$ kill -9 PID
...
$ pkill -9 PNAME

Tareas de Rake para congelar versiones especificas de Rails

Posted by joahking
on Aug 08, 08

Llega un momento en que actualizar tus proyectos a una nueva version de Rails es problematico y decides que deberias mantenerlos en la version especifica en la que los desarrollaste. Para ello tienes a tu disposicion tareas de Rake

Freezing a la version de Rails actual del sistema

$ rake rails:freeze:gems
Esta copia las gemas de Rails del sistema en el directorio vendor/rails de la aplicacion. Y cada vez que levantes el server lo haras desde esta version congelada.

Desechando la version congelada

Con esta otra desechas el Rails del directorio vendor/rails y vuelves a usar la del sistema.
$ rake rails:unfreeze

Freezing a una version especifica de Rails

Por ejemplo esta congela la aplicacion a usar Rails 2.0.2. Aqui la clave es que esos buenos chicos de Rails core taggean correctamente su desarrollo en GitHub:
$ rake rails:freeze:edge TAG=rel_2-0-2

Freezing para vivir al borde

O puedes ir desarrollando usando la version edge de Rails alojada en GitHub:
$ rake rails:freeze:edge

Determinar version congelada en la app

Una vez congelada una aplicacion es importante saber a que version lo has hecho:
$ script/about
# y la salida del comando sera:
About your application's environment
Ruby version              1.8.6 (i486-linux)
RubyGems version          1.2.0
Rails version             2.0.2
Active Record version     2.0.2
Action Pack version       2.0.2
Active Resource version   2.0.2
Action Mailer version     2.0.2
Active Support version    2.0.2
Edge Rails revision       rel_2-0-2
Application root          /home/joahking/dev/rails/aplicacion
Environment               development
Database adapter          mysql

Rails 2.1: trucos en el soporte UTC en ActiveRecord

Posted by joahking
on Aug 05, 08

Un poco de background sobre UTC en Rails 2.1 y ActiveRecord

Rails 2.1 viene con muy buen soporte para UTC que facilita mucho el hacer aplicaciones localizadas.

Es tan sencillo como declarar:

# config/environment.rb
config.time_zone = 'UTC' # o 'Madrid' por ejemplo

Tenemos a disposicion rake tasks que nos facilitan el trabajo:
$ rake -T time
rake time:zones:all    # Displays names of all time zones recognized by the...
rake time:zones:local  # Displays names of time zones recognized by the Rai...
rake time:zones:us     # Displays names of US time zones recognized by the ..
Para saber que timezones tenemos a disposicion hacemos:
$ rake time:zones:local
* UTC +01:00 *
Amsterdam
Berlin
...
Madrid
Paris
...
Luego para localizar la aplicacion, un before_filter cumple facil la tarea:
# controllers/application.rb
before_filter :set_time_zone

def set_time_zone
  Time.zone = @current_user.time_zone if @current_user
end
con su contraparte en las vistas para que el user seleccione su timezone:
# vale, TimeZone solo tiene us_zones pero algun hacker chovinista no tardara en mejorarlo
<%= f.time_zone_select :time_zone, TimeZone.us_zones %>
# y luego para mostrar la hora:
<%= Time.zone.now.inspect %>
Observa la diferencia entre Time.zone.now.inspect y Time.now.inspect:
# veamos la zona que tenemos activa
>> Time.zone
=> #<ActiveSupport::TimeZone:0xb7aab45c @name="Madrid", @tzinfo=#<TZInfo::DataTimezone: Europe/Madrid>, @utc_offset=3600>
# la fecha hora en formato español:
>> Time.zone.now.inspect
=> "Tue, 05 Aug 2008 10:34:10 CEST +02:00" 
# y en formato ingles:
>> Time.now.inspect
=> "Tue Aug 05 10:34:24 +0200 2008" 
Incluso podemos acceder el valor UTC de la fecha hora de un objeto antes de la conversion a la time zone con before_type_cast:
# campo datetime convertido a la time zone
>> m.created_at
=> Wed, 30 Jul 2008 14:41:28 CEST +02:00
# y el valor UTC archivado en la base de datos:
>> m.created_at_before_type_cast
=> "2008-07-30 12:41:28" 

A este valor al convertirlo a nuestra timezone simplemente habra que adicionarle el desplazamiento en husos horarios.

Tenemos mas helpers aun a disposicion:
# el constructor de fechas con numeros
>> Time.zone.local(2008, 8, 5, 10, 48, 18)
=> Tue, 05 Aug 2008 10:48:18 CEST +02:00
# el parseo pero con la timezone
>> Time.zone.parse('2008-08-05 10:48:18')
=> Tue, 05 Aug 2008 10:48:18 CEST +02:00
>> Time.zone.at(1207792098)
=> Thu, 10 Apr 2008 03:48:18 CEST +02:00
# y las horas en la timezone activa (Madrid)
>> t = Time.now
=> Tue Aug 05 10:50:47 +0200 2008
>> t.in_time_zone
=> Tue, 05 Aug 2008 10:50:47 CEST +02:00
>> t.in_time_zone('Madrid')
=> Tue, 05 Aug 2008 10:50:47 CEST +02:00
# para encontrar la hora en otra timezone por diferencias horarias con UTC
>> t.in_time_zone(+3.hours)
=> Tue, 05 Aug 2008 11:50:47 AST +03:00

Al respecto se han publicado interesantes articulos.

Trucos con el ActiveRecord

Sin embargo la cosa no va tan sobre rieles al encuestar al ActiveRecord pues este no hace conversiones a UTC por defecto. Veamoslo con ejemplos:
# inicialicemos
>> time_str = "2008-07-30T09:44:28+02:00" 
>> time = Time.parse time_str
Si creamos un objeto cualquiera vemos que su created_at se guarda en UTC en la bd:
# son las 9:44 en Madrid
# en bd queda este valor created_at => '2008-07-30 07:44:28'

O sea, la fecha hora se guarda con hora UTC y luego segun la timezone que declaremos se le suman los desplazamientos en hora. El erb si que aprueba con sobresaliente en darnos soporte con la conversion a UTC, pero ActiveRecord tiene sus trucos.

Si ahora hacemos esta query:
>> Xxx.find(:all, :conditions => ["created_at >= ?", time_str])
# esta es la query mal construida en el log:
SELECT * FROM "xxxs" WHERE (created_at >= '2008-07-30T09:44:28+02:00')
Esta claro que 2008-07-30T09:44:28+02:00 es la hora ya con su desplazamiento horario. Lo mismo ocurre mal con:
>> Xxx.find(:all, :conditions => ["created_at >= ?", time])
Concluyendo: ActiceRecord no hace uso de UTC por defecto en todos los frentes, asi que necesitamos darle la vuelta usando time.utc en la condition:
>> Xxx.find(:all, :conditions => ["created_at >= ?", time.utc])
# ahora si se ve correctamente en el log:
SELECT * FROM "xxxs" WHERE (created_at >= '2008-07-30 07:44:28')

Merb, Datamapper y RSpec: instalando edge versions

Posted by joahking
on Jul 15, 08

Merb el framework en Ruby para hackers y su ORM de moda Datamapper se encuentran en carrera hacia la version 1.0 que saldra segun Ezra este verano. Estos dos proyectos siguen la inteligente filosofia de separar en modulos y gemas cada funcionalidad, para que vayas cargando en tu environment las funcionalidades segun las vayas necesitando. Asi se mantienen las apps ligeras y con performances ultrarapidos.

Como queremos estar al dia con lo ultimo que vaya apareciendo tendremos que instalar directamente desde los repos en GitHub. La mejor manera de seguir un proyecto en sus avances hasta su release estable es vivir en el borde. Asi que olvidemosnos por un momento del comodo gem install GEMA y procedamos como los hackers.

Para mas detalles refierete al libro online de Merb o a Mr. Google.

Instalando las dependencias de Merb

Primero instalemos las gemas de las que depende Merb:
sudo gem install rack mongrel json erubis mime-types rspec hpricot mocha rubigen haml markaby mailfactory ruby2ruby

La gema json_pure es necesaria para ejecutar Merb con Jruby, la implementacion de Ruby en Java. Si no es tu caso json es mas eficiente, pero si mas adelante al instalar te la exigen instalala tambien.

Si tienes problemas instalando alguna gema chequea estos dos articulos anteriores sobre el tema.

Instalando Merb desde Github

Primero descargamos Merb de sus repositorios en GitHub.
git clone git://github.com/wycats/merb-core.git
git clone git://github.com/wycats/merb-plugins.git
git clone git://github.com/wycats/merb-more.git
Seguidamente instalamos cada parte en las que viene Merb separado:
cd merb-core ; rake install ; cd ..    
cd merb-more ; rake install ; cd ..
cd merb-plugins; rake install ; cd ..

Instalando las dependencias de Datamapper

DataMapper ha sido separado en las gemas dm-core y dm-more, la gema datamapper ya esta desactualizada.

Si tienes una version antigua de datamapper, data_objects, do_mysql (do_postgres o do_sqlite3 segun el adaptador que vayas a usar), y un merb_datamapper anterior a la version 0.9: debes eliminarlas antes de seguir.

Datamapper depende de las gemas extlib y data_objects. Comencemos por descargar estas de Github e instalarlas. La gema extlib depende de la gema english y data_objects de addressable, asi que si quieres ir mas rapido antes haz:
sudo gem install english addressable

Sino sigue adelante y ya te enteraras de las gemas que te falten. Descargamos extlib y data_objects y los instalamos.

git clone git://github.com/sam/extlib.git  
git clone git://github.com/sam/do.git

cd extlib
rake install ; cd ..

cd do
cd data_objects
rake install ; cd ..
cd do_mysql  # || do_postgres || do_sqlite3
rake install
En caso de que te falten gemas de las que dependa la que estes instalando veras mensajes asi:
(in /home/joahking/dev/merb/do/data_objects)
ERROR:  While executing gem ... (RuntimeError)
    Error instaling pkg/data_objects-0.9.1:
    data_objects requires addressable >= 1.0.3 
rake aborted!

Con que instales la gema mencionada estaras ok para luego repetir rake install.

Problema instalando el adaptador do_sqlite3

Si estas en una distro de Debian y ves este mensaje:
ERROR:  Error installing pkg/do_sqlite3-0.9.3:
    ERROR: Failed to build gem native extension.

/usr/bin/ruby1.8 extconf.rb install --local pkg/do_sqlite3-0.9.3 --no-update-sources
checking for sqlite3.h... no
Que dice que te falta alguna libreria, en este caso es el paquete libsqlite3-dev, instalalo con:
sudo apt-get install libsqlite3-dev

y de nuevo rake install. Este error es similar a los de do_postgres y do_mysql en caso que te decidas por alguno de estos adaptadores.

Instalando Datamapper

Ya se ha dicho que Datamapper cumple con el principio de venir bien separado en gemas y modulos para que los incluyas en tus modelos segun los necesites. Descarguemos ambas partes e instalemoslas:
git clone git://github.com/sam/dm-core.git
git clone git://github.com/sam/dm-more.git

cd dm-core ; rake install ; cd ..
cd dm-more
rake install

Instalando RSpec

RSpec es un framework Ruby para desarrollar siguiendo la filosofia Behaviour Driven Development que a muy grosso modo consiste en ir escribiendo las especificaciones de tu codigo o proyecto a la par de que lo implementas.

Merb es agnostico en cuanto a ORM, libreria de JavaScript y lenguaje de templates. Tambien es asi para el test framework que desees usar (test-unit, mocha, shoulda, rr). Aqui nos decidiremos por RSpec. Te recomiendo lo instales asi podras seguirme en la serie de articulos sobre Desarrollo Agil que planeo publicar.

git clone git://github.com/dchelimsky/rspec.git
cd rspec
rake gem
sudo gem install pkg/rspec-VERSION.gem
Al lanzar rake gem te saldra esta info:
  Successfully built RubyGem
  Name: rspec
  Version: 1.1.4
  File: rspec-1.1.4.gem

donde Version es la que usaras en el comando gem install pkg.... Y ya lo tienes todo.

Para actualizar todas las gemas

Para actualizar todas las gemas desde los fuentes, te vas al directorio donde las descargaste y:
git pull ; rake install

Problemas al actualizar las gemas

En subsecuentes updates del codigo base cuando intentes el rake install podria aparecer este error originado por el hecho de que ya tenias instaladas las gemas.
ERROR:  While executing gem ... (OptionParser::InvalidOption)
    invalid option: --no-update-sources
rake aborted!
Command failed with status (1): [sudo gem install --local pkg/data_objects-...]
/home/joahking/dev/merb/do/data_objects/Rakefile:33
(See full trace by running task with --trace)
El problema es la opcion --no-update-sources del gem en la linea indicada en el Rakefile:
# en Rakefile
  task :install => [ :package ] do
    sh %{#{SUDO} jruby -S gem install --local pkg/#{spec.name}-#{spec.version} --no-update-sources}, :verbose => false
  end
Yo soluciono esto cambiandome a una nueva branch del git:
git checkout -b quitando_opcion_no_update_sources
y en esta branch quitar la opcion de todos los Rakefile. Con un script como este me evito hacerlo manualmente:
for file in `find . -name Rakefile`; do sed 's/--no-update-sources//g' $file > "$file"_tmp; mv "$file"_tmp $file; done

y de nuevo rake install. buena suerte hacker!

Gem install falla debido a mkmf

Posted by joahking
on Jul 14, 08

Estas instalando una gema y te salta un error como este de que no existe mkmf

extconf.rb:1:in `require': no such file to load -- mkmf (LoadError)
    from extconf.rb:1

si usas alguna distro de debian fijate que mkmf es parte del paquete ruby1.8-dev. Instalalo con:

sudo apt-get install ruby1.8-dev

y ya estas good to go.

Instalando mongrel en una distro Debian

Posted by joahking
on Jul 11, 08
Quieres instalar mongrel y te salta esto:
/usr/lib/ruby/1.8/i486-linux/ruby.h:40:21: error: stdlib.h: No such file or directory
/usr/lib/ruby/1.8/i486-linux/ruby.h:44:21: error: string.h: No such file or directory
/usr/lib/ruby/1.8/i486-linux/ruby.h:54:19: error: stdio.h: No such file or directory
Primero asegurate que tienas la ultima version de rubygems. Luego si usas una distro de Debian necesitas este paquete:
$ sudo apt-get install build-essential

Problemas con un Rubygems desactualizado

Posted by joahking
on Jul 11, 08

Utilizar un sistema de manejo de gemas desactualizado puede traer muchos problemas extraños. Has un gem -v y si estas a menos que la 1.2.0 entonces estas desactualizado. Es sabido que los paquetes oficiales de Debian lo estan.

Actualizando rubygems desde los fuentes

Descargate la ultima version de los fuentes desde rubyforge y luego lo mas facil:
$ sudo ruby setup.rb 

pero cuidado pues esto puede romperte el sistema de gemas si ya lo tenias instalado, lo mejor seria segun el README:

$ sudo gem update --system

dos veces. Pero este desgraciadamente solo aparece despues que usaste la anterior.

Si rompiste el sistema de gemas

¿Y entonces te salta otro error mas extraño aun?
/usr/bin/gem:23: uninitialized constant Gem::GemRunner (NameError)

Lo mas simple es verificar si el error se refiere a un gem antiguo que convive con el recien instalado. Si asi es elimina el antiguo gem del sistema y haz un link simbolico al recien instalado.

Si quieres arreglar el antiguo la solucion es adicionar despues de la linea require 'rubygems' en /usr/lib/gem esta otra
require 'rubygems'
# adicionamos la dependencia
require 'rubygems/gem_runner'

aunque quizas esto traiga consigo otros errores in the long run.

Javascript No Obstrusivo o cuando Ajax es un problema

Posted by joahking
on Jul 08, 08
Ajax ha sido un boom en el desarrollo web en los últimos tiempos, pero mal usado puede crear problemas de rendimiento. Puedes probar una busqueda en Google, pero los puntos problematicos son estos más o menos:

Por ejemplo, los Selects en Cascada mejorarían su rendimiento si mandara al browser todos los datos necesarios y construyera los selects usando OPTGROUP evitando pedir pequeñas cantidades por Ajax cada vez para repintar los selects dependientes cada vez que se cambia la opcion de un padre.

Esto es precisamente lo que intenta resolver Ryan Bates en su railscast Dynamic Select Menus. Su idea es en la vista pedirle javascript a un controlador javascript:
<!-- views/somethings/new.html.erb -->
   <% javascript 'esta_accion' %>

# application_helper.rb
def javascript(*files)
  content_for(:head) { javascript_include_tag(*files) }
end

# y en el javascript_controller
def esta_accion
  @datos = Dato.find :all
end

Luego el erb parseará el views/javascripts/esta_accion.js.erb que se encargará de generarnos un javascript a la medida.

Hasta aquí todo bien, pero esto no nos resuelve el problema completamente. Mirando los logs notamos que para generar la vista new de los somethings estamos haciéndole dos peticiones al Rails.

Fíjate que no solo estamos hablando de mas de una petición a Rails, sino tambien si el proceso es más complejo que mis simples selects estaré repitiendo código del negocio de la acción new de Something en otro controlador.

O sea que eliminamos los muchos accesos al DOM y el Ajax pero a muy mal precio, ¿porqué no reducimos el round-trip a solo una request?

Unobstrusive Javascript

no voy a extenderme diciendo qué es el Javascript No Obstrusivo, para eso están los gurus y Mr. Google.

Resumiendo la idea es sacar el javascript del html y pegarle eventos a los elementos desde un javascript después que la página esté cargada:

<!-- algún html define el elemento -->
   <button id="alertable">Click me!</button>

<!-- algún javascript le pega el evento onclick -->
  document.observe(
     "dom:ready", 
     function() {
         document.getElementById("alertable").addEventListener( 'click',
            function() { 
               alert("Gracias, he esperado toda mi vida este momento!"); 
            }, 
         false);
      });
Hay un screencast muy bueno en railsenvy sobre cómo integrar UJS en Rails. Quedaría así resumiendo:
<!-- en el layout -->
<head>
    <%= yield :unobstrusive_javascript %>
</head>

<!-- views/algos/show.html.erb -->
<% content_for :unobtrusive_javascript do -%>
  <script type="text/javascript" charset="utf-8">
    document.observe(
        "dom:ready", 
        arrancaUserInterface('<%= @datos_para_la_UI.to_json %>'), 
        false
    );
  </script>
<% end %>

Ahora ya no necesitamos más el javascripts_controller, estoy contento con esta solución pero igual me falta la elegancia de la solución de Ryan Bates, donde no tenía javascript en el head de mi html, sino separado en un archivo javascript.

Talvez UJS4Rails es lo que estoy buscando, pero…

Si este fuera el mejor de los mundos posibles ¿cómo querría que esto quedara?

Me gustaría que en el procesamiento MVC de la petición Rails transformara la V en V + js, así tendriamos unobstrusive js out of the box, REST y algunas otras golosinas. Pero quien sabe si el futuro….

A este otro habria que darle una oportunidad

El plugin cascading javascripts carga en el javascript_include_tag macro los archivos javascript siguientes (si existen) en este orden:

  • application.js
  • #{controller_name/action_name}.js (ej. home/index.js, customers/new.js, etc).

Merbities: Procesamiento en el background

Posted by joahking
on Jun 27, 08

En frameworks web es importante ser capaces de responder al usuario rapido y dejar tareas mas pesadas en el background para ser ejecutadas mas tarde. En Merb creen que esto debiera proveerlo un framework web out of the box y Ezra puso manos a la obra al instante.

Merb::Worker

Es tan sencillo como llamar al metodo run_later en una accion de un controller:

#en un controlador
def accion 
  @foo = Foo.all 
  run_later do 
     procesamiento_demorado_con(@foo) 
  end 

  display @foo # o render 
end

El blog que pases sera guardado para ser ejecutado en un hilo en el background. Puedes llamar varias veces run_later para demorar varios blocks de ejecucion. Mas info en la pagina oficial de Merb