homeASCIIcasts

202: Consultas ActiveRecord en Rails 3 

(view original Railscast)

Other translations: En Cn De It

Other formats:

Written by Juan Lupión

En los últimos dos episodios hemos visto cómo configurar nuestro equipo y crear nuevas aplicaciones con Rails 3. En este episodio empezaremos a ver algunas de sus nuevas características empezando por ActiveRecord, que trae una nueva interfaz para realizar consultas a la base de datos. Pratik Naik ha tratado este tema con gran detalle en esta reciente anotación en su blog que merece la pena leer.

Algunos ejemplos básicos

Para empezar vamos a ver algunos ejemplos de las antiguas llamadas a find de ActiveRecord y las vamos a convertir al nuevo formato de consulta. Para esto consideraremos una aplicación Rails básica que incorpore dos modelos: Article y Comment que estarán relacionados de manera que un Article tiene has_many :comment.

Empezaremos con el find que devuelve los diez artículos publicados más recientes.

Article.find(:all, :order => "published_at desc", :limit => 10)

El enfoque básico para convertir una consulta ActiveRecord al nuevo formato de Rails 3 consiste en examinar el hash de opciones que se le pasa a find y reemplazar cada uno de sus elementos por un método equivalente. Así, en lugar de llamar a find como arriba ahora podemos utilizar:

Article.order("published_at desc").limit(10)

Como puede verse es fácil convertir los antiguos find de Rails a la nueva sintaxis, pero con la ventaja de que ésta última es más clara.

En el próximo ejemplo veremos que no siempre es posible mapear el antiguo hash de opciones con los nuevos métodos.

Article.find(:all, :conditions => ["published_at <= ?", Time.now], :include => :comments)

Sólo hay dos excepciones a la regla anterior, y el ejemplo anterior (de forma oportuna) tiene las dos. La sentencia anterior recuperará todos los artículos que tienen una fecha publicada anterior a la fecha actual así como su comentarios asociados. En Rails 3 tenemos que hacer:

En lugar de :conditions ahora utilizamos el método where pasándole los mismos argumentos que utilizaríamos con :conditions. Los argumentos se pueden pasar como un array pero es más fácil pasarlos por separado. Para recuperar los registros asociados como con :include se ha cambiado al plural para convertirse en el método includes . El resto de opciones que por lo general pasaríamos al método find se convierten en métodos con el mismo nombre que la opción.

Nuestro último ejemplo recupera el artículo publicado más recientemente.

Article.find(:first, :order => "published_at desc")

Con la nueva sintaxis de Rails se convierte en:

Article.order("published_at desc").first()

Obsérvese que no llamamos a first hasta el final de la cadena de métodos.

Dado que estamos recuperando registros en orden descendiente podríamos reescribir la línea anterior como:

Article.order("published_at").last()

Este código, un poco más conciso, realizará la misma consulta.

En Rails 3.0 podemos utilizar los antiguos métodos find o la nueva sintaxis de Rails 3, pero los métodos antiguos quedarán a extinguir en la versión 3.1 y en la 3.2 se eliminarán por completo. Merece la pena reescribir nuestros find durante el proceso de migración de nuestras aplicaciones a Rails 3 de forma que nuestras aplicaciones sean compatibles con las siguientes versiones de Rails 3.

En este punto podríamos preguntarnos cuál es el motivo de utilizar esta nueva sintaxis. Pues bien, hay un propósito detrás de todo esto, y reside en el poder de la carga perezosa (N. del T: en inglés, lazy loading).

Carga perezosa

Para mostrar en qué consiste la carga perezosa utilizaremos la consola de nuestra aplicación. Si preguntamos por todos los registros recuperaremos, como era de esperar, un array y veremos que tenemos tres artículos en nuestra base de datos.

ruby-1.9.1-p378 > Article.all => [#<Article id: 1, name: "It's Ancient", published_at: nil, hidden: false, 
created_at: "2010-02-22 20:35:42", updated_at: "2010-02-22 20:35:42">, 
#<Article id: 2, name: "Can't See Me", published_at: nil, hidden: false, 
created_at: "2010-02-22 20:37:03", updated_at: "2010-02-22 20:37:03">, 
#<Article id: 3, name: "To the Future!", published_at: nil, hidden: false, 
created_at: "2010-02-22 20:38:17", updated_at: "2010-02-22 20:38:17">]

Podemos obtener todos los artículos en orden alfabético utilizando el método order:

ruby-1.9.1-p378 > articles = Article.order("name")
 => #<ActiveRecord::Relation:0x00000101669b90 @table=#<Arel::Table:0x000001023e9af8 
@name="articles", @options={:engine=>#<Arel::Sql::Engine:0x000001023a15c8 @ar=ActiveRecord::Base,
@adapter_name="SQLite">}, @engine=#<Arel::Sql::Engine:0x000001023a15c8 @ar=ActiveRecord::Base,
@adapter_name="SQLite">, …

Esta vez en lugar de recibir una lista de artículos tenemos un objeto de tipo ActiveRecord::Relation, que guarda información sobre la consulta, pero ésta aún no ha sido ejecutada. Este es, pues, el significado de carga perezosa en este contexto: los datos no se cargan hasta que sea necesario. Si por ejemplo enumerásemos los registros con each, o los recuperásemos todos (con all), o tal vez sólo el primero (first) entonces sí que se lanzaría la consulta.

ruby-1.9.1-p378 > articles.first
 => #<Article id: 2, name: "Can't See Me", published_at: nil, hidden: false, 
created_at: "2010-02-22 20:37:03", updated_at: "2010-02-22 20:37:03">

Veamos cómo afecta esto a nuestra aplicación. Hemos generado el andamiaje necesario para el modelo de artículo, de forma que tenemos un controlador para los artículos con las siete acciones habituales. El código de la acción index utiliza Article.all para obtener todos los articulos:

/app/controllers/articles_controller.rb
def index
  @articles = Article.all

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @articles }
  end
end

Como vimos antes si sobre estos artículos usamos alguna de las opciones de búsqueda, como por ejemplo ordenar por nombre, entonces se devuelve un objeto de tipo ActiveRecord::Relation y la consulta no será ejecutada en el controlador sino que se lanzará desde el código de la vista cuando recorramos con each el listado de artículos.

/app/views/articles/index.html.erb
<% @articles.each do |article| %>
  <tr>
    <td><%= article.name %></td>
    <td><%= article.published_at %></td>
    <td><%= article.hidden %></td>
    <td><%= link_to 'Show', article %></td>
    <td><%= link_to 'Edit', edit_article_path(article) %></td>
    <td><%= link_to 'Destroy', article, :confirm => 'Are you sure?', :method => :delete %></td>
  </tr>
<% end %>

Si ahora cargamos la página de índice los artículos aparecerán ordenados alfabéticamente.

The scaffold-generated article index page.

Lo interesante de esto es que si estamos usando caché de fragmentos en nuestras vistas (con el método cache) se produce una importante mejora del rendimiento porque la consulta a la base de datos no se ejecutará a no ser que el fragmento esté expirado.

Con la nueva sintaxis nos será mucho más fácil construir condiciones complejas. Por ejemplo, supongamos que queremos filtrar los artículos de forma que aparezcan sólo los artículos ocultos si en la URL se recibe hidden=1 como parámetro. Podemos hacerlo modificando la acción index de la siguienta manera:

/app/controllers/articles_controller.rb
def index
  @articles = Article.order('name')
    
  if params[:hidden]
    @articles = @articles.where(:hidden =>(params[:hidden] == "1"))
  end

  respond_to do |format|
    format.html # index.html.erb
    format.xml  { render :xml => @articles }
  end
end

Comprobamos si hay un parámetro hidden, y de haberlo añadimos un método where a la consulta que mostrará sólo los artículos ocultos si el parámetro tiene el valor 1. Si añadimos dicho parámetro a la URL y recargamos la página veremos únicamente los artículos ocultos.

Mostrando sólo los artículos ocultos.

Igualmente si pasamos el parámetro 0 tan sólo veremos los artículos visibles.

Ahora sólo se muestran los artículos visibles.

Esta es una forma muy elegante de construir consultas más complejas a la base de datos sabiendo que dichas consultas no se ejecutarán realmente hasta que los datos sean necesarios.

Ámbitos nominales

A continuación veremos algunos de los cambios realizados sobre los ámbitos nominales (N. del T: en inglés, named scopes) en Rails 3. Abajo aparece nuestro modelo Article con dos ámbitos definidos, uno para recuperar los artículos visibles y otro para recuperar los artículos que han sido publicados.

/app/models/article.rb
class Article < ActiveRecord::Base
  named_scope :visible, :conditions => ["hidden != ?", true]
  named_scope :published, lambda { {:conditions => ["published_at <= ?", Time.zone.now]} }
end

Estos ámbitos nominales se definen igual que haríamos en una aplicación 2.x pero en Rails 3 el enfoque es un poco diferente. La primera diferencia que nos encontramos es que el método para definir un ámbito nominal ya no es named_scope sino unicamente scope. Tampoco necesitamos ya pasar las condiciones como un hash sino que al igual que con find podemos utilizar métodos. Tal y como hicimos con los nuevos métodos de find utilizaremos where en lugar de :conditions. En la sintaxis de Rails 3 los ámbitos nominales ahora tienen el siguiente nombre:

/app/models/article.rb
class Article < ActiveRecord::Base
  scope :visible, where("hidden != ?", true)
  scope :published, lambda { where("published_at <= ?", Time.zone.now) }
end

Otra funcionalidad nueva es la posibilidad de agregar ámbitos. Si queremos crear un ámbito llamado recent que devuelva los artículos visibles más recientes ordenados por fecha de publicación podemos hacerlo reutilizando los dos ámbitos que ya tenemos.

scope :recent, visible.published.order("published_at desc")

Lo que hemos hecho es encadenar los dos ámbitos que ya teníamos y añadir el método order para crear nuestro nuevo ámbito. Se trata de una funcionalidad muy poderosa cuando estemos definiendo los ámbitos que tendrán nuestros modelos.

Podemos intentar probar nuestro ámbito recién creado. Si invocamos Article.recent se devuelve un objeto de tipo ActiveRecord::NamedScope::Scope. Este objeto se comporta de forma similar a los objetos ActiveRecord::Relation que vimos anteriormente.

ruby-1.9.1-p378 > Article.recent
 => #<ActiveRecord::NamedScope::Scope:0x0000010318bd08 @table=#<Arel::Table:0x00000102740ea8 
 @name="articles", @options={:engine=>#<Arel::Sql::Engine:0x00000102651900 @ar=ActiveRecord::Base>}, 
 @engine=#<Arel::Sql::Engine:0x00000102651900 @ar=ActiveRecord::Base>>, …

Si invocamos all en el objeto Scope veremos que se devuelven los artículos correspondientes.

ruby-1.9.1-p378 > Article.recent.all
 => [#<Article id: 1, name: "It's Ancient", published_at: "2010-01-01", 
 hidden: false, created_at: "2010-02-22 20:35:42", updated_at: "2010-02-22 23:00:16">]

Un último consejo

Acabaremos este episodio con un truco muy útil. Si tenemos un objeto Relation o Scope y queremos ver la consulta SQL que se lanzaría contra la base de datos se puede invocar su método to_sql.

ruby-1.9.1-p378 > Article.recent.to_sql
 => "SELECT     \"articles\".* FROM       \"articles\" 
 WHERE     (hidden != 't') AND (published_at <= '2010-02-22 22:47:12.023289') 
 ORDER BY  published_at desc"

Esto nos mostrará el SQL que enviará ActiveRecord para devolvernos los artículos publicados más recientemente que no estén ocultos.

Y eso es todo por este episodio que ha tratado del uso de consultas ActiveRecord en Rails 3. Por supuesto hay muchas más novedades que harán que el código de nuestros controladores Rails sea más conciso y epxresivo. Recuperaremos con creces el tiempo que dediquemos a jugar con estas nuevas funcionalidades.