#354 Squeel
- Download:
- source codeProject Files in Zip (75.5 KB)
- mp4Full Size H.264 Video (34.1 MB)
- m4vSmaller H.264 Video (14.2 MB)
- webmFull Size VP8 Video (13.6 MB)
- ogvFull Size Theora Video (36.6 MB)
Supongamos que tenemos una página que muestra un listado de productos. La página incluye una caja de texto para realizar búsquedas de productos por su nombre.
El código que implementa la búsqueda parece sencillo de escribir pero la consulta en el modelo Product
es bastante complicada. Tenemos que armar una búsqueda para devolver aquellos productos que: han sido publicados, no han sido retirados, se encuentran en stock y cuyo nombre coincide con el término de búsqueda.
class Product < ActiveRecord::Base belongs_to :category attr_accessible :name, :price, :category_id, :released_at, :discontinued_at, :stock def self.search(query) where("released_at <= ? and (discontinued_at is null or discontinued_at > ?) and stock >= ? and name like ?", Time.zone.now, Time.zone.now, 2, "%#{query}%") end end
Estamos lanzando una consulta SQL compleja con muchas variables vinculadas que deben ser consistentes con los parámetros de la propia consulta. Hay varias formas de mejorar esta consulta, y lo mejor sería mover la consulta a ámbitos con nombre. Si lo hiciéramos, todavía tendríamos que seguir preocupándonos por las diferencias de comportamiento de SQL como por ejemplo la cláusula LIKE
. En la mayoría de bases de datos se lanzará una consulta que no distingue entre mayúsculas y minúsculas, por lo que si tuviéramos que cambiar el motor de base de datos a Postgres tendríamos que cambiarlo por ILIKE
. Sería bueno tener aquí un poco más de consistencia, de forma que pudiésemos abstraer estas consultas a la base de datos. Podríamos usar Arel, pero su uso directo en estos casos se nos puede complicar bastante.
Si no queremos mezclar SQL con nuestro código Ruby podemos utilizar la gema Squeel, escrita por Ernie Miller que también escribió la gema Metawhere que ya vimos en el episodio 251. Squeel aporta un DSL que nos permite definir las búsquedas en Ruby sin tener que recurrir a SQL. Para usarlo, tendremos que actualizar el Gemfile
y ejecutar bundle install
.
gem 'squeel'
Empezaremos jugando con Squeel en la consola. Squeel permite pasar un bloque a una llamada where
, dentro del cual podremos utilizar el DSL de Squeel. Podemos, por ejemplo, invocar cualquier columna como un método:
> Product.where{released_at <= 3.months.ago} Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."released_at" <= '2012-03-08 20:58:21.852780' => [#<Product id: 1, name: "Settlers of Catan", category_id: 2, price: #<BigDecimal:7fdbf9b90aa0,'0.3495E2',18(45)>, released_at: "2012-03-01 00:00:00", discontinued_at: nil, stock: 5, created_at: "2012-06-08 20:09:13", updated_at: "2012-06-08 20:58:13">]
Esto se traduce a una consulta SQL que devuelve los productos coincidentes. Nótese que la convención del DSL de SQueel, y que nosotros seguimos, es no añadir espacios alrededor de las llaves de la consulta. El objeto devuelto es de tipo ActiveRecord::Relation
que se puede combinar normalmente con el resto de ámbitos de ActiveRecord. Pero Squeel va más allá de ActiveRecord::Relation
, usando Arel para convertir la consultas en SQL. Esto se explica en el README de Squeel donde se muestra una tabla con la lista de operadores soportados, así como el operador SQL equivalente. Así que en lugar de usar el operador <
de Squeel podemos usar el método lt
de Arel directamente:
> Product.where{released_at.lt 3.months.ago} Product Load (0.3ms) SELECT "products".* FROM "products" WHERE "products"."released_at" < '2012-03-08 21:29:36.133638' => [#<Product id: 1, name: "Settlers of Catan", category_id: 2, price: #<BigDecimal:7fdbf96d3940,'0.3495E2',18(45)>, released_at: "2012-03-01 00:00:00", discontinued_at: nil, stock: 5, created_at: "2012-06-08 20:09:13", updated_at: "2012-06-08 20:58:13">]
Otra funcionalidad útil de Squeel son los operadores AND y OR para combinar múltiples condiciones. Si queremos cambiar la busqueda de forma que sólo encuentre elementos con un costo de más de 20 dólares podemos hacerlo así:
> Product.where{released_at.lt(3.months.ago) & price.gt(20)} Product Load (0.6ms) SELECT "products".* FROM "products" WHERE (("products"."released_at" < '2012-03-09 08:41:15.786451' AND "products"."price" > 20)) => [#<Product id: 1, name: "Settlers of Catan", category_id: 2, price: #<BigDecimal:7fe0aa2d5a40,'0.3495E2',18(45)>, released_at: "2012-03-01 00:00:00", discontinued_at: nil, stock: 5, created_at: "2012-06-08 20:09:13", updated_at: "2012-06-08 20:58:13">]
Nótese que cuando se usan diferentes términos de búsqueda como en este caso es importante rodear los términos de búsqueda con paréntesis de forma que el intérprete de Ruby siga la precedencia correcta. Lo bueno de esto es que hace que sea mucho más fácil utilizar el operador OR. Puede ser difícil hacerlo en ActiveRecord, pero con Squeel es inmediato:
> Product.where{released_at.lt(3.months.ago) | price.gt(20)} Product Load (0.4ms) SELECT "products".* FROM "products" WHERE (("products"."released_at" < '2012-03-09 08:44:05.427791' OR "products"."price" > 20) # Se omiten los productos
Uso de Squeel en nuestra aplicación
Ahora que ya tenemos suficiente información para adaptar la consulta de nuestra aplicación con Squeel. Este es el aspecto que tiene ahora mismo.
def self.search(query) where("released_at <= ? and (discontinued_at is null or discontinued_at > ?) and stock >= ? and name like ?", Time.zone.now, Time.zone.now, 2, "%#{query}%") end
Traducido a Squeel el código queda así:
def self.search(query) where do (released_at <= Time.zone.now) & ((discontinued_at == nil) | (discontinued_at > Time.zone.now)) & (stock >= 2) & (name =~ "%#{query}%") end end
Nótese que con la cláusula LIKE utilizamos =~
, similar al operador de expresión regular, que realizará una búsqueda sin distinguir mayúsculas o minúsculas independientemente del motor de base de datos que utilicemos. También usamos ==nil
para comparar con NULL en lugar de is null
.
Se puede discutir si este código es más claro que el que teníamos antes, pero una mejora clara es que los valores se encuentran embebidos en la consulta en lugar de añadirse por el final. También al estar usando código Ruby podemos utilizar un bloque de múltiples líneas para que la consulta quede más legible. Veremos que si realizamos la misma consulta en el navegador vuelven a aparecer los mismos resultados que devolvía nuestra consulta SQL.
Todo en su contexto
Conviene no olvidar que Squeel utiliza instance_eval
cuando invoca al bloque. Esto quiere decir que el contexto del bloque no será la clase Product
sino una instancia del DSL de Squeel. Si queremos invocar un método de clase dentro del bloque no podremos hacerlo directamente porque Squeel lo interpretará como un nombre de columna. Para superar esto tenemos que invocar my
con un bloque. Todo lo que vaya dentro de este bloque será evaluado en el contexto original, para demostrar esto vamos a añadir el método low_stock
a nuestra clase y lo emplearemos en la búsqueda.
class Product < ActiveRecord::Base belongs_to :category attr_accessible :name, :price, :category_id, :released_at, :discontinued_at, :stock def self.search(query) where do (released_at <= Time.zone.now) & ((discontinued_at == nil) | (discontinued_at > Time.zone.now)) & (stock >= my{low_stock}) & (name =~ "%#{query}%") end end def self.low_stock 2 end end
Este código realizará la misma búsqueda que antes.
Cómo adaptar Squeel
Existe un generador que creará un inicializador donde podremos modificar el comportamiento de Squeel.
$ rails g squeel:initializer create config/initializers/squeel.rb
Esta orden crea un fichero de configuración donde se incluyen comentarios que explican las diferentes opciones. Por ejemplo si se descomenta la línea que se muestra a continuación se añadirán métodos a las clases Hash y Symbol para simular la funcionalidad de Metawhere (lo cual nos puede ser útil si estamos migrando nuestra aplicación para que use Squeel en lugar de Metawhere).
# To load both hash and symbol extensions: # # config.load_core_extensions :hash, :symbol
En este fichero también podemos añadir alias de forma que podríamos por ejemplo invocar less_than
en lugar de lt
.
# Alias an existing predicate to a new name. Use the non-grouped # name -- the any/all variants will also be created. For example, # to alias the standard "lt" predicate to "less_than", and gain # "less_than_any" and "less_than_all" as well: # # config.alias_predicate :less_than, :lt
Con esto cerramos nuestro repaso de Squeel. Hay, como siempre, mucha más funcionalidad que la que hemos visto hoy, por lo que se recomienda leer toda la documentación a los que vayan a usar Squeel en sus aplicaciones.
Squeel es un proyecto muy bonito, por supuesto es posible que las mejoras al lenguaje de consulta no nos resulten de interés si nos encontramos cómodos con SQL, pero los que prefieran trabajar con Ruby en lugar de SQL verán que merece la pena.