¿Cómo puedo reducir el número de Queries utilizando RubyOnRails con Active Records? joins, include – Eager loading Associations


Naturaleza caprichosaLo que voy a explicar en este post, es fruto de algunas preguntas que suelen ocurrir cuando quieres mejorar los resultados de consultas a la Base de Datos y conversaciones entre compañeros de profesión. Normalmente cuando te preguntas ¿cómo puedo hacer que estos no me arroje tantas queries en una consulta que estoy haciendo?  o ¿Cómo puedo hacer que entre dos tablas sacar algunos campos que sólo me interesan extraer esos datos y no todos?

Para lo que voy a exponer, necesito ponerte en situación. Las tablas que necesitamos son, productos en nuestro modelo Product y que tendremos asociadas unas categorías en una tabla de categorías en nuestro modelo de Category  y tendremos que también puede darse el caso que necesitemos crear comentarios a nuestros productos y en ese caso tendremos nuestra tabla de comentarios en nuestro modelo de Review y claro está que no nos podía faltar nuestros usuarios en el modelo User. Bien con esto ya tenemos definido nuestro schema de la base de datos con la que vamos a trabajar.

Vamos a inspeccionar nuestro modelo de productos Product. Lo primero es que necesitamos tener accesibles una serie de atributos como el nombre, el precio y el id de una categoría y después nuestras relaciones necesarias con otras tablas que formar parte como ya sabes de nuestro Active Record. Las relaciones que se obtienen de nuestra tabla de productos son, las categorías que decíamos antes, un producto pertenece a una categoría, belongs_to :category, también necesitamos una relación con comentarios, es decir que un producto puede tener muchos comentarios, has_many :reviews.

Veamos como nos queda nuestro modelo después de lo comentado:

class Product < ActiveRecord::Base
attr_accessible :name, :price, :category_id
 belongs_to :category
               has_many :reviews

end

Y nuestro controlador index en el que necesitamos ver todos los productos, a que categoría pertenece el producto y que precio tiene nuestro producto. El método index tendrá:

def index
      @products = Product.order(“name”)
end

para que cuando lo pase a nuestra vista asociada a nuestro controlador, obtengamos el resultado del listado. Pero, tenemos que fijarnos en la línea product.category.name que realmente nos proporciona el dato del nombre de la categoría asociada a un producto:

          h1>Products</h1>

<table id=”products”>
<tr>
<th>Product Name</th>
<th>Category</th>
<th>Price</th>
</tr>
<% @products.each do |product| %>
<tr>
<td><%= link_to product.name, product %></td>
          <td><%= product.category.name %></td>
<td class=”price”><%= number_to_currency(product.price) %></td>
<td><%= link_to “edit”, edit_product_path(product) %></td>
</tr>
<% end %>
</table>

Vale pero esto ¿qué nos aporta como resultado? como resultado así a simple vista no sabemos como lo ha hecho. Tenemos que ir a nuestra consola de resultados, si usamos un IDE como RubyMine pues vemos que en la consola tenemos un resultado que creo interesante comentarlos para comprender el siguiente paso que abordaremos. Fíjate cuando lo hagas el resultado que tenemos en el SQL. Lo primero y para obtener los productos un SELECT “products”.* FROM “products” ORDER BY name y después por cada registro de productos, en nuestro caso tenemos un total de 37 productos, nos arroja 37 SQL a categorías, es decir, SELECT “categories”.* FROM “categories” WHERE “categories”.”id” = 7 LIMIT 1, variando el resultado con WHERE “categories”.”id” = 7, 10, ….  las categorías asociadas a cada producto.

Si lo piensas es un poco inapropiado y pensado que puede crecer el número de productos, el resultado de las queries a categorías, escala de forma exponencial en proporción al crecimiento de nuestros registros de productos.

¿Cómo puedo mejorar esos resultados y que no se hagan tantas SQL?

Para eso existe un técnica que se llama “Eager loading Associations”  en la que consiste en cargar, con el menor número de queries posibles, los objetos devueltos en un modelo Find. Por si quieres echar un vistazo a las guías de Rails te dejo este enlace que habla de esto mismo.Vamos a probar a poner un include en nuestra SQL utilizando en nuestro controlador de producto lo siguiente:

@products = Product.order(“name”).includes(:category)

¿Qué obtenemos como resultado?

Pues reducir el número de SQL que decíamos anteriormente y a que Active Record haga 2 únicos pasos. Fíjate ahora el resultado de consola:

Started GET “/products” for 127.0.0.1 at 2013-03-28 18:37:40 +0100
Processing by ProductsController#index as HTML
Product Load (0.4ms) SELECT “products”.* FROM “products” ORDER BY name
Category Load (0.2ms) SELECT “categories”.* FROM “categories” WHERE “categories”.”id” IN (7, 6, 10, 2, 4, 9, 5, 3, 8)
Rendered products/index.html.erb within layouts/application (22.2ms)
Completed 200 OK in 34ms (Views: 33.0ms | ActiveRecord: 0.6ms)

Observa que ahora nos hace el SQL para los productos SELECT “products”.* FROM “products” ORDER BY name y después nos hace in IN de las categorías por sus ID SELECT “categories”.* FROM “categories” WHERE “categories”.”id” IN (7, 6, 10, 2, 4, 9, 5, 3, 8) y lo hemos conseguido mediante el include(:category) siendo category la propia asociación que tenemos en el modelo de productos como belongs_to :category para que sepa a lo que nos estamos refiriendo. Podríamos hacer algunas variantes al respecto utilizando un array de múltiples asociaciones por ejemplo Post.includes (:category, :comments)

Otra variante es utilizar en este tipo de técnica con un JOIN @products = Product.order(“categories.name”).joins(:category).select(“products.id, products.name, products.price, categories.name as category_name”)

En este caso podemos también poner en la vista  category_name de la parte del select que corresponde al campo de categories.name y cambiar product.category.name por la nueva denominación que hemos establecido como as category_name

<% @products.each do |product| %>
<tr>
<td><%= link_to product.name, product %></td>
          <td><%= product.category.name %></td> <td><%= product.category_name %></td>
<td><%= number_to_currency(product.price) %></td>
<td><%= link_to “edit”, edit_product_path(product) %></td>
</tr>

En consola podemos utilizar algunas JOIN con más asociaciones interesantes como Product.joins(:category, :reviews => :user) ya que la relación es a través review hacia los usuarios ya que tiene un belongs_to :user y no está asociada a productos para hacer directamemte como en los casos de :category y :reviews, por eso ponemos :reviews => :user.

Para terminar un aclaración entre usar include y usar join, si usamos INCLUDE y la condición no se cumple, por ejemplo, tenemos un producto que no tiene comentarios, los productos se cargan Product.include(:category, :reviews). Pero si esto mismo utilizamos JOIN Product.joins(:category, :reviews) y no hay comentarios, no se cargan los productos. Pensemos que según nos interese, podemos utilizar una u otra con esos pequeños detalles.

Para hacer algunas aclaraciones más, he expandido el post a : Complemento a Eager loading Associations y entender las diferencias entre Join, Left Join y Right Join

Un comentario en “¿Cómo puedo reducir el número de Queries utilizando RubyOnRails con Active Records? joins, include – Eager loading Associations

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s