Novedades en la próxima versión de Ruby on Rails: 3.1 (II)

Ahora que Rails 3.1 ha alcanzado ya la Release Candidate, y que la publicación de la versión final es inminente, qué mejor momento para retomar aquella lista de nuevas características que nos trae esta nueva edición del famoso framework para desarrollar aplicaciones web en Ruby.

No te pierdas tampoco la primera parte de este especial.

Migraciones reversibles más sencillas

Siguiendo la metodología DRY ("Don't Repeat Yourself"), la definición de ciertas migraciones para cambiar el esquema de nuestra base de datos pasa a ser más sencilla, de modo que no sea necesario repetir instrucciones en los dos bloques de la migración (el de ejecución de la misma y el de deshacer los cambios) cuando éstas puedan ser fácilmente deducidas.

Esto significa que podremos especificar los cambios deseados en una tabla en un nuevo método change, y Rails se encargará de crear los métodos up y down por nosotros. Por cierto, estos últimos pasan a ser métodos de instancia, en lugar de métodos de clase. Si Rails encuentra ciertos comandos en el método change que no puede deshacer (por ejemplo, si se ejecuta un remove_column que elimina una columna de la tabla, y añadirla al deshacer no sería suficiente dado que se habría perdido la información que ésta contenía al ejecutar la migración), entonces se lanza una excepción ActiveRecord::IrreversibleMigration.

Por ejemplo, donde antes escribíamos algo como:

  class CreateTasks < ActiveRecord::Migration
    def self.up
      create_table :tasks do |t|
        t.string :title
        t.boolean :completed
        t.text :description
        t.timestamps
      end
    end
    
    def self.down
      drop_table :tasks
    end

Ahora escribiríamos simplemente:

  class CreateTasks < ActiveRecord::Migration
    def change
      create_table :tasks do |t|
        t.string :title
        t.boolean :completed
        t.text :description
        t.timestamps
      end
    end

Identity Map en ActiveRecord

Una de las características que más me gusta de DataMapper (un ORM bastante chulo y, hasta la introducción de Arel en Rails, con mejor sintaxis que ActiveRecord a mi parecer) es precisamente Identity Map. Esta característica sencillamente mantiene un hash con todas las instancias que se han hecho de una determinada clase que hereda de ActiveRecord, de modo que sólo exista en memoria una representación en forma de objeto en Ruby de un registro de la tabla en particular.

¿Por qué esto es importante? Porque hasta ahora, si instancias un objeto ActiveRecord, y llamas por ejemplo a un método que nuevamente instancia otro objeto el cual representa al mismo registro de la base de datos, y este último realiza un cambio en alguno de sus atributos, dicho cambio no se ve reflejado en el primero, porque ocupan espacios en memoria diferentes (a todos los efectos, son dos instancias distintas que representan a un mismo registro).

Para verlo más claro con código. En ActiveRecord sin Identity Map, se cumple que:

  post1_instance_1 = Post.find(1) # => #<Post id: 1, title: "Rails 3.1">
  post1_instance_2 = Post.find(1) # => #<Post id: 1, title: "Rails 3.1">

# Parece que son la misma instancia, porque ActiveRecord redefine el método de comparación # y lo que hace es comprobar que los valores de los atributos sean iguales. post1_instance_1 == post1_instance_2 # => true

# Pero verdaderamente, son instancias diferentes post1_instance_1.object_id == post1_instance_2.object_id # => false

Esto tiene una segunda consecuencia, y es que aunque dos consultas devuelvan instancias de registros repetidos para los que ya teníamos objetos en memoria, éstos se volvían a instanciar ocupando su propio espacio, devorando de forma aún mayor los recursos de memoria de la máquina, y obligando a la aplicación a consultar siempre los valores de la base de datos cuando se quiere instanciar un objeto.

Gracias a Identity Map, ActiveRecord consume menos memoria, es más rápido, y cada registro tiene una representación única en forma de objeto Ruby, de modo que cualquier cambio en el mismo sea consistente. No obstante, esta característica no estará habilitada por defecto debido a algunos problemas aún existentes con la instanciación de objetos a través de asociaciones.

Engines que pueden ser montados en una ruta determinada

Rails::Engine ha sido mejorado aún más, y ahora es posible montar una aplicación organizada en forma de engine en una ruta de una aplicación Rails. Estoy seguro de que esto mejorará aún más las posibilidades de reutilización de código y funcionalidades entre aplicaciones.

Por ejemplo, ahora podemos tener un engine que proporciona la funcionalidad de un blog, y montarlo en nuestra aplicación bajo la ruta "/blog" añadiendo en nuestro fichero routes.rb:

  Rails.application.routes.draw do
    mount Blog::Engine => "/blog"
  end

Mejoras en la utilización de las cabeceras HTTP para caché

A partir de esta release, Rack::Cache será parte del core del framework, y se espera que sustituya a la caché de páginas en un futuro (la cual podría quedar relegada a un plugin). Las ventajas de Rack::Cache para la obtención condicional del contenido de una página son evidentes: Con la utilización de las cabeceras HTTP Expires, Cache-Control, Last-Modified y ETag, es posible indicar de diferentes formas la validez de un determinado contenido, y evitar tener que enviar de nuevo toda la página cuando ésta es la misma que el navegador tiene en su caché, incluso aunque la misma se encuentre cacheada en forma de HTML y sólo haya que enviar su contenido sin tener que regenerarlo.

Evidentemente, esto permite ahorrar un mayor ancho de banda, y atender así más peticiones por segundo desde nuestra instancia de la aplicación.

Serializers personalizados

Si antes querías convertir un Hash, un Array, o cualquier otra instancia de un objeto Ruby en un objeto YAML, para poder serializarlo y guardarlo en un atributo de tipo text en la base de datos, sólo tenías que usar el método serialize de ActiveRecord::Base. Pero, ¿y si preferías guardar el valor en algún otro formato, uno que te permitiese guardar su valor en una forma que te facilitase futuras consultas en la base de datos, o que sencillamente ofusque un poco los datos? Tenías que construir una serie de métodos en tu modelo para asignar y leer los valores cada vez.

Ahora puedes dejar la mayor parte del trabajo a ActiveRecord, utilizando el mismo método serialize, pero especificando como parámetro un objeto que responda a los métodos load y dump, donde definir el algoritmo a utilizar para convertir el objeto en cuestión a texto y para generarlo de nuevo a partir del valor serializado. Por ejemplo:

  class Post < ActiveRecord::Base
    class CommaSeparatedList
      def load(text)
        return unless text
        text.split(",")
      end

      def dump(text)
        text.join(",")
      end
    end

    serialize :tags, CommaSeparatedList.new
  end
  
  post = Post.create(:tags => ["rails", "ruby"])
  # INSERT INTO "posts" ("tags", "created_at") VALUES ('rails,ruby')

Más información | Rails 3.1 Release Candidate

Portada de Genbeta