Aug 05 Rails 2.1: trucos en el soporte UTC en ActiveRecord

tags: rails activerecord utc timezones | comments

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

Update

sobre esto hay buenas nuevas, el ticket que cree al respecto ha sido aceptado y asignado a Koz

mira los detalles en lighthouse

blog comments powered by Disqus