Interfaz de consultas de Active Record
Agiliza tu tiempo de desarrollo con queries eficientes y naturales.
Bien sea que haz realizado consultas sencillas con lenguaje SQL para obtener registros en alguna base de datos relacional o tienes experiencia como administrador de base de datos complejas, al iniciar podría parecer extremadamente difícil aprender la sintaxis para seleccionar solo lo que requerías, o que tabla unir con cual para realizar la consulta necesaria.
En este artículo vamos a conocer la magia que realiza Ruby junto a su gema “ActiveRecord” para automatizar y facilitar el manejo de los registros en una base de datos relacional.
Cuando ActiveRecord realiza el mapping de las clases que hereden de la clase ActiveRecord::Base, construye métodos de consulta comunes en simples métodos de clase. Los mismos queries SQL son realizados por ActiveRecord, pero los abstrae a métodos tales como find(), group(), select(), entre otros, en los cuales nosotros como desarrolladores no vemos lo que está sucediendo.
Te darás cuenta que podrás realizar las mismas operaciones de una manera mas rápida y con una sintaxis mucho mas natural. Yo mismo, sentí que era mucho mas fácil ya que requería escribir menos código además que dichos métodos eran nombrados de una manera mas intuitiva. Por ejemplo, si queremos obtener el libro con el id: 10, en lenguaje SQL seria así:
SELECT * FROM books WHERE (books.id = 10) LIMIT 1
Con la sintaxis del método find() de ActiveRecord seria:
Book.find(10)=> #<Book id: 10, title: "Civil Brand", author: "Sauncho Uglow">
El resultado es exactamente el mismo, obtienes el registro del libro con el id solicitado, con la ventaja que la sintaxis es mucho mas corta y fácil de entender.
Acabamos de utilizar un método de ActiveRecord para realizar un query sencillo que encuentre un registro en la tabla “Books” y con ello pudimos ver un poco de la magia en acción, sin embargo no se observó en el terminal el query que se ejecutó para obtener ese registro.
En el día a día como desarrollador web y sobretodo al trabajar con base de datos complejas y con tablas con muchas relaciones se hace necesario mostrar el query en la terminal. ¿Así que como podemos ver que esta sucediendo realmente?.
Clase Logger de ActiveRecord::Base
Para ayudar a los desarrolladores a ver los queries que realmente se están realizando fue creada la clase Logger. La utilizas de esta manera:
ActiveRecord::Base.logger = Logger.new(STDOUT)
Es importante siempre tratar de pensar en cuál sería el query SQL que se está realizando ya que esto ayuda a reforzar las habilidades SQL y te hace pensar en la consulta paso por paso.
Practicando consultas con ActiveRecord y Logger
Además de obtener solo un registro, al método find le puedes enviar un arreglo de ids, veamos el ejemplo y su SQL equivalente:
Book.find([1, 5])=> # SELECT "books".* FROM "books" WHERE "books"."id" IN (?, ?) [["id", 1], ["id", 5]]=> [#<Book id: 1, title: "Chelsea on the Rocks", author: "Ellerey Woolacott">, #<Book id: 5, title: "French Fried Vacation 3", author: "Quint Gantzer">]
Como podemos ver, al ejecutar el método find se muestra el query SQL que se ejecutó, además del resultado obtenido, en esta ocasión, un Array con los 2 registros de la tabla “Books” que requeríamos.
Métodos esenciales para buscar registros en tu base de datos
- take: Retorna un registro sin ningún orden implícito.
Book.take=> # SELECT * FROM books LIMIT 1=> #<Book:
id: 0,
title: "Forever Yours (Ikuisesti sinun)",
author: "Anabal Firbanks">
- first: Retorna el primer registro ordenado por su clave primaria.
Book.first=> # SELECT * FROM books ORDER BY books.id ASC LIMIT 1=> #<Book:
id: 0,
title: "Forever Yours (Ikuisesti sinun)",
author: "Anabal Firbanks">
- last: Retorna el ultimo registro ordenado por su clave primaria(por defecto).
Book.last=> # SELECT * FROM books ORDER BY books.id DESC LIMIT 1=> #<Book: id: 50, title: "Alien Abduction", author: "Kerry Bensen">
En una colección que este ordenada utilizando el método order, last va a retornar el ultimo registro ordenado por el atributo especificado.
Book.order(:title).last=> # SELECT * FROM clients ORDER BY clients.first_name DESC LIMIT 1=> #<Book:
id: 20,
title: "Vive L'Amour (Ai qing wan sui)",
author: "Orton Trever">
- find_by: Retorna el primer registro encontrado que cumpla cierta condición.
Book.find_by(author: 'Susana Jeanesson')=> SELECT * FROM books WHERE (books.first_name = 'Susana Jeanesson') LIMIT 1=> #<Book: id: 2, title: "People, Places, Things", author: "Susana Jeanesson"
Búsquedas condicionales
El método where te permite especificar condiciones para limitar los registros retornados. Este método representa la parte WHERE de un query SQL.
Book.where("title = ?", "Black Sheep")=> # SELECT "books".* FROM "books" WHERE (title = 'Black Sheep')=> [#<Book: id: 3, title: "Black Sheep", author: "Jourdan Leckenby"
Si quieres especificar múltiples condiciones
Detail.where("quantity > ? AND price > ?", 49, 10 )=> # SELECT "details".* FROM "details" WHERE (quantity > 47 AND price > 10)=> # [#<Detail: id: 41, quantity: 50, book_id: 15, price: 194,7>, #<Detail: id: 45, quantity: 50, book_id: 39, price: 180,6>, #<Detail: id: 50, quantity: 50, book_id: 6, price: 137,4>]
Para hacerlo incluso mas entendible, puedes utilizar las condiciones con un hash y el resultado será exactamente el mismo
Detail.where(quantity: 49, price: 10)
Al método where se le puede enviar un rango como condición
Detail.where(quantity: 25..29)=> # [#<Detail: id: 10, quantity: 29, book_id: 42, invoice_id: 22, price: 0.1294e2>, #<Detail: id: 24, quantity: 26, book_id: 1, invoice_id: 22, price: 0.1007e2>]
Búsquedas ordenadas
Como vimos anteriormente podemos utilizar el método order, te permite ordenar los registros, basándose en un parámetro indicado.
Invoice.order(:date)=> # SELECT "invoices".* FROM "invoices" ORDER BY "invoices"."date" ASC=> #[#<Invoice: id: 42, date: Sat, 23 Jun 2018, is_paid: true, customer_id: 1>, #<Invoice: id: 18, date: Tue, 10 Jul 2018, is_paid: false, customer_id: 48>, #<Invoice: id: 39, date: Sat, 28 Jul 2018, is_paid: true, customer_id: 4>,
Seleccionando un atributo
Al realizar una búsqueda puedes indicar los atributos específicos que necesitas con el método select.
Invoice.select("date")=> # SELECT "invoices"."date" FROM "invoices"[#<Invoice: id: 0, date: Thu, 06 Sep 2018>, #<Invoice: id: 1, date: Sat, 19 Jan 2019>, #<Invoice: id: 2, date: Sun, 25 Nov 2018>]
Limitando la búsqueda
Con limit y offset puedes indicar un límite máximo de registros, así como indicar el numero de registros a ignorar antes de iniciar la búsqueda.
Country.limit(3).offset(10)=> SELECT "countries".* FROM "countries" LIMIT ? OFFSET ? [["LIMIT", 3], ["OFFSET", 10]=> [#<Country: id: 11, name: "Jamaica">, #<Country: id: 12, name: "Brazil">, #<Country: id: 13, name: "Chad">]
Agrupar y contar los registros
Para aplicar las clausulas SQL GROUP BY ó COUNT podemos utilizar los métodos group y count de ActiveRecord.
Book.group(:status).count=># SELECT COUNT(*) AS count_all, “books”.”status” AS books_status FROM “books” GROUP BY “books”.”status”=> # {“available”=>18, “unavailable”=>15, “promotion”=>18}
Scopes
Los scopes te permiten especificar consultas de uso muy común, a las que se puede hacer referencia al invocarse. En ellos se pueden utilizar todos los métodos mencionados anteriormente.
class Book < ActiveRecord::Base scope :expensive, -> { where(price: 90..100) }end
Al realizar la invocación de ese scope definido en el modelo Book, sucede lo siguiente:
Book.expensive=> # SELECT "books".* FROM "books" WHERE "books"."price" BETWEEN ? AND ? [["price", 90], ["price", 100]]=> # [#<Book: id: 3, title: "Black Sheep", author: "Jourdan Leckenby", price: 95>, #<Book: id: 6, title: "10 Mountains 10 Years", author: "Rheta Esposi", price: 97>, #<Book: id: 26, title: "Little Big Man", author: "Alberik Willerstone", price: 93>
Los scopes pueden recibir parámetros que modifiquen su funcionamiento
class Customer < ActiveRecord::Base scope :defaulter, ->(debt) { where("debt > ?", debt) }end
Al realizar la invocación de ese scope definido en el modelo , sucede lo siguiente:
Customer.defaulter(950)=> # SELECT "customers".* FROM "customers" WHERE (debt > 950)[#<Customer:
id: 2,
name: "Patton Aynsley",
debt: 968>, #<Customer:
id: 30,
name: "Lotti Blint",
debt: 995>, #<Customer:
id: 31,
name: "Etty Tremblet",
debt: 989>
Enums
Los “macros” enum asigna a una columna entera de una tabla, un conjunto de valores enteros.
class Book < ActiveRecord::Base enum status: [:available, :unavailable, :promotion]end
Automáticamente esto definirá algunos scopes correspondientes a cada una de las opciones del enum. Veamos su funcionamiento:
# Consultamos por el último libro registradobook = Book.last=> #<Book: id: 50, title: "Alien Abduction", author: "Kerry Bensen", price: 0.86e2, status: "available", publisher_id: 39># Podemos consultar si el libro esta en status: :available, al utilizar el scope que te crea el enum por defecto.book.available?=> true# Además podemos cambiar el status a cualquiera de las opciones del enum invocando una de las opciones con el "bang". A dicho libro se le actualizará su :status.book.unavailable!Book Update (0.3ms) UPDATE "books" SET "status" = ? WHERE "books"."id" = ? [["status", 2], ["id", 50]]=> true# Si volvemos a preguntar si posee el status: :available, obtenemos un false, ya que se actualizó previamentebook.available?=> false
Buscando por SQL
Si deseas utilizar tus propios queries SQL para encontrar registros de una tabla de tu base de datos, puedes utilizar el método find_by_sql.
Customer.find_by_sql("SELECT * FROM customers WHERE (customers.country_id = 40)")=> [#<Customer:
id: 2,
name: "Patton Aynsley",
country_id: 40>, #<Customer:
id: 25,
name: "Eva Gullis",
country_id: 40>
Recomendaciones finales
Luego de conocer algunos de los métodos mas utilizados para realizar consultas a la base de datos con ActiveRecord te puedo recomendar que realices muchas prácticas para que logres reducir el tiempo de desarrollo de una aplicación además de consultar la documentación de Active Record Query Interface, donde encontrarás la utilidad, la sintaxis adecuada, y los argumentos de cada método de consulta.