Delegando atributos de ActiveRecord

November 4th, 2008 | by | programación

Nov
04

El otro día tratando de mejorar un modelo de una aplicación me topé con el problema de tener muchos atributos virtuales para poder acceder a atributos de un objeto relacionado y no caer en lo siguiente :

class Player < ActiveRecord::Base
  belongs_to :user
 
  def name
    user.name
  end
 
  def email
    user.email
  end
 
  # ... y varios más
end

Esto no lo puedo evitar mucho ya que Player es una clase intermedia en una relación de N-M entre los usuarios y los partidos, y además contiene información necesaria para la lógica del modelo.

Tampoco era muy feliz tener por todos lados Message Chains:

@player.user.name
@player.user.email
# etc ....

Una de las recomendaciones que se usan en estos casos es Hide Delegate para ocultarle al cliente de donde sale el dato realmente. Cabe aclarar que no siempre son un problema las llamadas encadenadas.

Recordando el anuncio de Rails 2.2 noté que al final hablaba de un delegate que tenía un nuevo feature. Buscando un rato por google encontré este post donde hablaba de un método delegate para hacer justamente esto que yo quería.

Lo extraño, que también menciona el autor es que no está documentado en la API oficial de Rails aunque mirando el código veo que explica como usarla. Explícitamente dice :

Provides a delegate class method to easily expose contained objects' methods as your own

En la documentación también aclara que es útil tanto para atributos propios como para asociaciones entre diferentes instancias de ActiveRecord. La realidad es que por cómo está implementada funciona para cualquier objeto ruby que se nos ocurra.

El ejemplo con el que empezamos el post quedaría resumido a :

class Player < ActiveRecord::Base
  belongs_to :user
  delegate :name, :email, ... , :to => :user
end

Quedando mucho más corto el código, por lo tanto más fácil de mantener.

Tomando prestado los ejemplos del otro blog, también podemos hacer cosas con atributos que no son asociaciones, como por ejemplo una fecha :

# Forma abreviada
class Content < ActiveRecord::Base
  delegate :year, :month, :day, :to => :published_at
end
 
# Forma desglosada
class Content < ActiveRecord::Base
  delegate :year, :to => :published_at
  delegate :month, :to => :published_at
  delegate :day, :to => :published_at
end
 
# Podemos escribir
@content.year
 
# en lugar de
@content.published_at.year

2 Comments »

Lo que viene en Rails 2.2

October 27th, 2008 | by | programación

Oct
27

Hace poco se anunció el RC1 de lo que será la versión 2.2 de Ruby on Rails. Si bien la fecha de salida es “cuando esté lista”, ya se pueden utilizar para aquellos que gusten vivir “on the edge”.

Para aquellos que quieran un rejunte completo de todo lo que se viene pueden ver el post : What’s New in Edge Rails: Rails 2.2 Released – Summary of Feature.

Yo acá voy a comentar solo lo que tuve la oportunidad de probar y que me resultó útil.

I18n

La verdad es que es una buena noticia, no para mi porque hago todo para un solo idioma :P , pero algún día puede resultar útil. Lo único que a mi me deja con sabor a poco es la forma en que se hace que no me termina de gustar. Antes que nada quiero aclarar, porque ya vi la pregunta en un par de listas de correo, que esto localiza textos, no el contenido de una instancia de ActiveRecord. Si uno quiere que el contenido que cargan los usuarios sea traducido, es otra historia :) .

Las traducciones se escriben en ruby o en archivos YAML, con “hashes” que pueden estar anidados (como si fueran “namespaces“) :

# lib/locale/en-US.rb
{ 'en-US' => {
  :hello_world => "Hello World",
  :hello_flash => "Hello Flash"
}}
 
# lib/locale/pirate.rb
{ 'pirate' => {
  :hello_world => "Ahoy World",
  :hello_flash => "Ahoy Flash"
}}

Y luego en las vistas (o mailers o donde sea) en lugar de poner el texto se ingresa el symbol asociado al texto que cargamos en las traducciones, por ejemplo :

<h1>< %=t :hello_world %></h1>

Mi problema particular con este método es que estoy mucho más acostumbrado a cómo se hace con gettext (que no necesariamente es la mejor forma) donde se ponen todos los textos en inglés en la aplicación y después se escriben las traducciones.

Hay un demo online acá, que en estos momentos está caído, espero que para cuando lean esto ya este funcionando de nuevo.

Join Tables Conditions

Esto es algo que realmente hacía falta. Vamos a ver la mejora con un ejemplo. Supongamos que tenemos los siguientes modelos :

class Article < ActiveRecord::Base
  belongs_to :user
end
 
class User < ActiveRecord::Base
  has_many :articles
end

y queremos obtener todos los usuarios que tengan al menos un artículo publicado. Para eso deberíamos hacer :

User.find(:all, :joins => :article,  :conditions => ["articles.published = ?", true])

Con la nueva sintaxis es posible especificar este tipo de queries de una manera más amena como sigue :

User.find(:all, :joins => :article, :conditions => { :articles => { :published => true } })

ActionMailer Layouts

Algo que si me tocó vivir es tener varios emails HTML con un mismo formato y que cambiaba el contenido (Tu amigo te invitó, Tu nueva clave es, Ganaste un premio, etc). Con el mailer actual casi que hay que hacer un copy & paste del marco y despuer cambiar para cada tipo de email la lógica de qué se muestra.

En rails 2.2 ahora vamos a tener layouts como los tenemos en ActionView, de manera de tenerlo una vez y si arreglamos o cambiamos algo cambia para todos.

Memoization

Es muy frecuente que uno agregue lógica simple de caching en los modelos para las variables de instancia como se ve en el siguiente ejemplo :

  class User < ActiveRecord::Base
    def full_name
      @full_name ||= "#{first_name} #{last_name}"
    end
  end

Esto lo hacemos para evitar el overhead de crear el full_name si lo llegamos a usar varias veces en una misma vista, de manera que el string se crea en la primer llamada y en las siguientes solamente lo retorna.

Los puristas dicen que en realidad está mal, porque se está responsabilizando al método de algo que en realidad no debería importarle : la política de caching. ¿Suena exagerado? Seguramente :) .

Para solucionar esto se agregó el método memoize que nos permite a agregar a un método común este tipo de lógica de manera separada :

  class User < ActiveRecord::Base
    def full_name
      "#{first_name} #{last_name}"
    end
 
    memoize :full_name
  end

Esto se encarga de que cuando llamemos a @user.full_name se comporte de la misma manera que el primer ejemplo, sin tener que modificar el método. También nos permite saltarnos el "cache", por ejemplo si en la lógica acabamos de cambiar el nombre de pila, debemos forzar para que el nombre completo cambie :

   @user = User.new :first_name => 'Test', :last_name => 'Test'
   @user.full_name # => Test Test
   @user.first_name = 'Do'
   @user.full_name # => Test Test
   @user.full_name(true) # => Do Test

Y más

Hay varios cambios más, como Thread Safety, ETag, partial GETs y demás que a mi por ahora no me interesan y por eso no me entiendo mucho más. Pueden consultar el anuncio oficial para ver de que se tratan o cómo pueden hacerle la vida más fácil :) .

2 Comments »

Próxima reunión de Argentina on Rails

October 25th, 2008 | by | programación

Oct
25

El próximo sábado (1 de noviembre) nos estamos reuniendo para compartir, aprender y divertirnos un rato con ruby. El que quiera aparecer, que aparezca y si quiere avisar que va, mejor así calculo bien la cantidad de mates que tenemos que tener :) .

La cita es en la facultad de ingeniería de la UBA, aula 402 (4to piso) a partir de las 11hs. Más detalles de como se va a organizar la cosa acá (incluye un mapa para aquellos que no conozcan la facultad).

No Comments »

Usando Rails.cache en 2.1.x

October 21st, 2008 | by | programación

Oct
21

Para uno de los sitios que tenemos montados llegó la hora de optimizar algunas partes para descargar la DB un poco. Lo primero (y por ahora único :P ) que se hizo fue cachear la instancia que representa a la Etapa actual del juego en memoria, para así no cargarla en cada request.

El código original era :

class Stage < ActiveRecord::Base
  def self.current
    t = Time.now.utc
    find(:first, :conditions => ["start_at < = ? AND end_at >= ?", t, t])
  end
end

y lo cambiamos para usar el nuevo sistema de cache de Rails 2.1.x que realmente fue simplificado. Hay varios sitio donde hablan sobre el cache en 2.1, les recomiendo mirar el screencast de RailsCasts y este y este post.

El código modificado quedó como se muestra a continuación.

# config.environment.rb
config.cache_store = :mem_cache_store
 
# app/models/stage.rb
class Stage < ActiveRecord::Base
  def self.current
    @current = Rails.cache.read('stage_current')
 
    if @current.nil? || @current.closed?
      t = Time.now.utc
      @current = find(:first, :conditions => ["start_at < = ? AND end_at >= ?", t, t])
      Rails.cache.write('stage_current', @current)
    end
    @current
  end

La condición del if es necesaria por dos motivos : la primera por si el cache fue limpiado y la segunda para invalidar el valor guardado actualmente si la etapa eterminó, para que la nueva etapa pase a ocupar el cache.

En este caso estamos usando MemCache como CacheStore ya que se comparte entre más de un webserver y simplifica expirar el cache.

Lo único que resta es expirar el cache en caso de que cambie el modelo, que además de la fecha de finalización contiene textos que son usados en la web (como las bases y condiciones, premios, etc). Para eso creamos un Observer que se encargue de eso cuando una etapa es guardada y además es la etapa actual (si no, no tiene sentido hacer nada) :

# app/models/stage_observer.rb
class StageObserver < ActiveRecord::Observer
  def after_save(stage)
    Rails.cache.delete('stage_current') if stage.is_current?
  end
end
 
# config/environment.rb
config.active_record.observers = :stage_observer

Con esto bajó bastante el uso de la DB (prácticamente no se puede hacer nada en el sitio sin consultar la etapa actual). Ahora tengo que ver que le pasa a la DB que a veces respondes después de 3 segs :S, pero eso ya escapa a programar :) .

No Comments »

Refactoring de “Fat Methods” – Episodio 2

October 9th, 2008 | by | programación

Oct
09

Continuando con esta serie de ejemplos de como refactorizar métodos de controladores vamos a seguir ahora con uno que cuando lo volví a ver apestaba feo feo.

El método en cuestión tiene por objetivo que el usuario actual se suscriba (es decir, anote) para jugar un partido determinado. Para eso el siguiente código es el que está siendo ejecutado actualmente :

  # app/controllers/matches_controllers.rb
  def subscribe
    flash[:notice] = "Ya formás parte del equipo!" and \
        redirect_to match_path(@match) and \
        return if @match.has_player? current_user
 
    flash[:error] = "El equipo está completo." \
      and redirect_to match_path(@match) and ]
      return if @match.available_places(params[:team].to_i) < = 0
 
    @match.players.create :user => current_user, :team => params[:team]
 
    redirect_to match_path(@match)
  end

Lo feo del problema se puede ver desde dos perspectivas : una que es una excusa y otra que es la razón de la desprolijidad. La excusa para este código tan horrible es que en caso de no poder agregar el jugador al equipo elegido tengo que mostrar un mensaje de error y redireccionar. Pero el redirect_to lo que hace es setear un header nada más, es decir que no hace un return auto-mágico, y de no hacerlo yo el segundo redirect_to (ubicado al final del código) haría que se lance una Exception por haber reenviado los headers.

El problema real es que estoy delegando en el controlador la responsabilidad de la clase Player para determinar si es válido crear una instancia para un partido y usuario dado.

Es por eso que lo primero que debemos hacer es usar los validators de Rails para esta tarea :

  # app/models/player.rb
  def validate
    errors.add_to_base("Ya formás parte del equipo!") if match.has_player? user
    errors.add_to_base("El equipo está completo.") if match.available_places(team) < = 0
  end

En este caso los errores no dependen de un atributo, por lo que usamos add_to_base para que los errores digan lo que queremos. Este método validate es llamado por Rails al crear o actualizar una instancia de Player, y si hay algún error nunca llega a la DB.

Habiendo quitado la validación del controlador ahora podemos escribirlo de una forma más prolija y entendible :

  # app/controllers/matches_controllers.rb
  def subscribe
    @player = @match.players.create :user => current_user, :team => params[:team].to_i
 
    flash[:notice] = @player.errors.full_messages.join(", ") if @player.errors.any?
 
    redirect_to match_path(@match)
  end

Lovely :) . Se crea un Player para el Match, se prepara una alerta visual en caso de que haya algún error y luego se redirige a la página del partido. Si ven el antes y el después creo que nadie me va a negar de la mejora :) .

Por hoy es todo, hasta la próxima entrega!.

No Comments »