#251 MetaWhere & MetaSearch
- Download:
- source codeProject Files in Zip (118 KB)
- mp4Full Size H.264 Video (17 MB)
- m4vSmaller H.264 Video (10.2 MB)
- webmFull Size VP8 Video (25.4 MB)
- ogvFull Size Theora Video (23.7 MB)
A continuación se muestra una captura de pantalla de una aplicación sencilla en Rails 3 que muestra un listado de productos. La página tiene un buscador que filtra los productos por nombre.
La búsqueda se lleva a cabo en la acción index
del controlador ProductsController
, cuyo código tiene el siguiente aspecto:
def index @products = Product.where("name LIKE ?", "%#{params[:search]}%") end
Se utiliza el método where
con un fragmento de SQL para encontrar todos los productos cuyo nombre es similar al término de búsqueda. Dado que la condición de búsqueda es más compleja que una mera igualdad no podemos pasarla en un hash de condiciones, y lo mismo pasaría si estuviésemos haciendo una comparación del tipo "mayor o menor que", porque ActiveRecord sólo admite cadenas SQL para este tipo de consultas.
Si no nos convence la idea de usar fragmentos de SQL en nuestro código existe una gema llamada MetaWhere que puede sernos útil. MetaWhere permite invocar métodos sobre los símbolos de los argumentos que se le pasan a where
, por lo que podemos hacer consulta tales como:
Article.where(:title.matches => 'Hello%', :created_at.gt => 3.days.ago)
A partir del código anterior MetaWhere generará la siguiente consulta SQL:
SELECT "articles".* FROM "articles" WHERE ("articles"."title" LIKE 'Hello%') AND ("articles"."created_at" > '2010-04-12 18:39:32.592087')
Con MetaWhere se elimina la necesidad de utilizar fragmentos de SQL en nuestras consultas y podemos usar condiciones en un hash. Este enfoque es similar a como funcionan DataMapper o Mongoid, y es una opción interesante con ActiveRecord. Para ver lo fácil que es utilizar MetaWhere vamos a modificar, a modo de ejemplo, esta aplicación.
Lo primero que tenemos que hacer es añadir una referencia a la gema en el Gemfile
de la aplicación.
# Edit this Gemfile to bundle your application's dependencies. source 'http://gemcutter.org' gem "rails", "3.0.3" gem "sqlite3-ruby", :require => "sqlite3" gem "meta_where"
Tenemos que ejecutar la orden bundle
para instalar la gema. Tras esto podemos modificar el código de búsqueda en ProductsController
y eliminar el fragmento SQL.
def index @products = Product.where(:name.matches => "%#{params[:search]}%") end
Con este cambio tan sencillo la aplicación se comportará igual que antes, devolviendo los mismos resultados.
Veamos a continuación algunos ejemplos más complejos ejecutados en consola. Por ejemplo podemos recuperar todos los prodcutos cuyo precio es inferior a cinco libras.
ruby-1.9.2-p0 > Product.where(:price.lt => 5) +----+-----------------------+--------+-------------+------------+ | id | name | price | released_at | updated_at | +----+-----------------------+--------+-------------+------------+ | 6 | 1 Pint of Milk | 0.49 | 2010-06-06 | 2011-01-31 | | 7 | Porridge Oats | 1.99 | 2010-07-07 | 2011-01-31 | +----+-----------------------+--------+-------------+------------+ 2 rows in set
También podemos añadir condiciones OR
con múltiples hashes de condiciones separados por la barra vertical. La siguiente búsqueda muestra todos los prodcutos que cuestan menos de cinco libras o cuyo título incluye la palabra “video” .
ruby-1.9.2-p0 > Product.where({:price.lt => 5} | {:name.matches => "%video%"}) +----+-----------------------+--------+-------------+------------+ | id | name | price | released_at | updated_at | +----+-----------------------+--------+-------------+------------+ | 6 | 1 Pint of Milk | 0.49 | 2010-06-06 | 2011-01-31 | | 7 | Porridge Oats | 1.99 | 2010-07-07 | 2011-01-31 | | 8 | Video Game Console | 299.95 | 2010-08-08 | 2011-01-31 | | 9 | Video Game Disc | 29.95 | 2010-09-09 | 2011-01-31 | +----+-----------------------+--------+-------------+------------+ 4 rows in set
Con MetaWhere también disponemos de algunas extensiones útiles en el metodo order
. Si queremos ordenar los productos por su fecha released_at
para que nos muestre los productos más recientes, podemos hacerlo con el siguiente código.
ruby-1.9.2-p0 > Product.order(:released_at.desc) +----+-----------------------+--------+-------------+------------+ | id | name | price | released_at | updated_at | +----+-----------------------+--------+-------------+------------+ | 9 | Video Game Disc | 29.95 | 2010-09-09 | 2011-01-31 | | 8 | Video Game Console | 299.95 | 2010-08-08 | 2011-01-31 | | 7 | Porridge Oats | 1.99 | 2010-07-07 | 2011-01-31 | | 6 | 1 Pint of Milk | 0.49 | 2010-06-06 | 2011-01-31 | | 5 | Oak Coffee Table | 279.99 | 2010-05-05 | 2011-01-31 | | 4 | Black Leather Sofa | 499.99 | 2010-04-04 | 2011-01-31 | | 3 | Stereolab T-Shirt | 12.49 | 2010-03-03 | 2011-01-31 | | 2 | DVD Player | 79.99 | 2010-02-02 | 2011-01-31 | | 1 | All-New Log For Girls | 29.95 | 2010-01-01 | 2011-01-31 | +----+-----------------------+--------+-------------+------------+ 9 rows in set
Existe también una sintaxis alternativa para las condiciones de find
pero primero tenemos que activarla con
MetaWhere.operator_overload!
Una vez tengamos esta opción activada podemos utilizar los operadores estándar de Ruby en lugar de los métodos gt
y lt
. Por tanto ahora podemos ejecutar lo siguiente para encontrar los productos que cuestan menos de 5 libras:
ruby-1.9.2-p0 > Product.where(:price < 5) +----+-----------------------+--------+-------------+------------+ | id | name | price | released_at | updated_at | +----+-----------------------+--------+-------------+------------+ | 6 | 1 Pint of Milk | 0.49 | 2010-06-06 | 2011-01-31 | | 7 | Porridge Oats | 1.99 | 2010-07-07 | 2011-01-31 | +----+-----------------------+--------+-------------+------------+ 2 rows in set
De esta forma resulta más cómodo definir condiciones de búsqueda.
Por supuesto podemos hacer muchas más cosas con la gema MetaWhere, por lo que merece la pena leer la documentación para averiguar más.
MetaSearch
Dedicaremos el resto de este episodio a explorar otra gema del mismo autor: MetaSearch. Esta gema añade una forma muy útil de realizar búsquedas sobre un modelo desde un formulario que consiste en llamar a un método llamado search
sobre cualquier modelo pasándole los parámetros de búsqueda, tras lo que podemos recuperar los registros que casan con dicha consulta. Se muestra un ejemplo a continuación.
def index @search = Article.search(params[:search]) @articles = @search.all end
En el código de la vista el nombre de cada campo define la funcionalidad de búsqueda. Por ejemplo un campo de texto llamado title_contains
quiere decir que se buscarán los registros cuyo title
contenga el valor introducido por el usuario en dicho campo.
Probémoslo en nuestra aplicación cambiando nuestro formulario de búsqueda por uno que utilice MetaSearch. El primer paso, como en MetaWhere, es añadir una referencia a la gema en el Gemfile
.
/Gemfile
# Edit this Gemfile to bundle your application's dependencies. source 'http://gemcutter.org' gem "rails", "3.0.3" gem "sqlite3-ruby", :require => "sqlite3" gem "meta_where" gem "meta_search"
Tendremos otra vez que ejecutar bundle
para instalar la gema. A continuación podemos cambiar el código de búsqueda en ProductsController
por el código MetaSearch equivalente. El código tiene el siguiente aspecto:
def index @products = Product.where(:name.matches => "%#{params[:search]}%") end
Y lo cambiaremos por:
def index @search = Product.search(params[:search]) @products = @search.all end
Creamos la búsqueda invocando a Product.search
, pasándole los parámetros del formulario. A continuación invocamos a @search.all
para recuperar la lista de productos correspondiente, que usará una cosnulta SQL de forma inmediata. Si queremos añadir condiciones extra podemos usar relation
en lugar de all
, que devolverá un ámbito. Aunque en este caso no nos hace falta, si necesitamos paginar podemos utilizar paginate
.
Tenemos que modificar el formulario de búsqueda de la plantilla index
para usar los nombres de campo que necesita MetaSearch (el formulario utilizaba form_tag
, pero ahora tendremos que usar form_for
con el objeto devuelto por el método search
). Queremos buscar productos cuyo atributo name
contenga el término de búsqueda, por lo que el campo de texto en el formulario se tiene que llamar name_contains
.
<%= form_for @search do |f| %> <p> <%= f.label :name_contains %> <%= f.text_field :name_contains %> </p> <p class="button"><%= f.submit "Search" %></p> <% end %>
Si cargamos nuevamente la página de productos veremos el nuevo formulario, y si buscamos otra vez el mismo término de antes obtendremos los mismos resultados.
Lo más útil de esto es que se pueden expandir las opciones de búsqueda simplemente añadiendo más campos en la vista, no hace falta modificar nada ni en el modelo ni en el controlador. Si, por ejemplo, queremos añadir la posibilidad de buscar por un rango de precios sólo tenemos que cambiar el formulario para añadir los campos llamados price_gte
y price_lte
tal y como sigue:
<%= form_for @search do |f| %> <p> <%= f.label :name_contains %> <%= f.text_field :name_contains %> </p> <p> <%= f.label :price_gte, "Price ranges from" %> <%= f.text_field :price_gte, :size => 8 %> <%= f.label :price_lte, "to" %> <%= f.text_field :price_lte, :size => 8 %> </p> <p class="button"><%= f.submit "Search" %></p> <% end %>
Al volver a cargar la página veremos los dos nuevos campos. Podemos buscar los productos por rango de precios o por nombre y precio, por lo que si buscamos productos cuyo nombre contenga “video” y que cuesten entre diez y treinta libras, encontraremos el único producto que hay:
La clave aquí son los nombres de los campos en el formulario, porque determinan las condiciones de búsqueda. Hay más opciones para usar en los campos de formulario, descritas en la documentación.
Otra funcionalidad que añade MetaSearch es la posibilidad de ordenar los resultados por un campo concreto, usando el método sort_link
. Este método recibe dos argumentos: un objeto de búsqueda y un nombre de columna, con lo podemos añadir campos de ordenación con el siguiente código:
<p> Sort by: <%= sort_link @search, :name %> <%= sort_link @search, :price %> <%= sort_link @search, :released_at %> </p>
Al recargar la página veremos los enlaces por los cuales podemos ordenar los productos. También podemos combinar la ordenación con un filtro.
Seguridad
Al trabajar con MetaSearch debemos tener en cuenta que puede hacer que nuestras tablas de la base de datos admitan búsquedas sobre cualquiera de sus columnas, simplemente modificando el formulario antes de enviarlo. Esto también se aplica a las asociaciones, por lo que si tenemos registros asociados que contienen datos sensibles deberíamos ser conscientes de esto.
Para ayudarnos con este problema hay algunos métodos de seguridad que podemos añadir a nuestros modelos para limitar qué campos pueden buscarse. No entraremos en detalle pero estos métodos están descritos en el sitio web de MetaSearch. Necesitaremos implementar estos métodos si estamos usando MetaSearch en una web que está de cara al público.
Con esto terminamos nuestro repaso de MetaWhere y MetaSearch. Hay otra gema que proporciona una funcionalidad semejante llamada Searchlogic, que no funciona con Rails 3. Merece la pena considerar MetaWhere y MetaSearch si nos gusta la funcionalidad de Searchlogic pero necesitamos una solución para Rails 3.