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')
Comments

Leave a response

Comment