Interfaz de Consulta de Active Record

Esta guía cubre diferentes formas de recuperar datos de la base de datos utilizando Active Record.

Después de leer esta guía, sabrás:


1 ¿Qué es la Interfaz de Consulta de Active Record?

Si estás acostumbrado a usar SQL en bruto para encontrar registros de la base de datos, generalmente encontrarás que hay mejores formas de llevar a cabo las mismas operaciones en Rails. Active Record te aísla de la necesidad de usar SQL en la mayoría de los casos.

Active Record realizará consultas en la base de datos por ti y es compatible con la mayoría de los sistemas de bases de datos, incluidos MySQL, MariaDB, PostgreSQL y SQLite. Independientemente del sistema de base de datos que estés utilizando, el formato del método de Active Record siempre será el mismo.

Los ejemplos de código a lo largo de esta guía se referirán a uno o más de los siguientes modelos:

CONSEJO: Todos los modelos siguientes usan id como clave primaria, a menos que se especifique lo contrario.

class Author < ApplicationRecord
  has_many :books, -> { order(year_published: :desc) }
end
class Book < ApplicationRecord
  belongs_to :supplier
  belongs_to :author
  has_many :reviews
  has_and_belongs_to_many :orders, join_table: 'books_orders'

  scope :in_print, -> { where(out_of_print: false) }
  scope :out_of_print, -> { where(out_of_print: true) }
  scope :old, -> { where(year_published: ...50.years.ago.year) }
  scope :out_of_print_and_expensive, -> { out_of_print.where('price > 500') }
  scope :costs_more_than, ->(amount) { where('price > ?', amount) }
end
class Customer < ApplicationRecord
  has_many :orders
  has_many :reviews
end
class Order < ApplicationRecord
  belongs_to :customer
  has_and_belongs_to_many :books, join_table: 'books_orders'

  enum :status, [:shipped, :being_packed, :complete, :cancelled]

  scope :created_before, ->(time) { where(created_at: ...time) }
end
class Review < ApplicationRecord
  belongs_to :customer
  belongs_to :book

  enum :state, [:not_reviewed, :published, :hidden]
end
class Supplier < ApplicationRecord
  has_many :books
  has_many :authors, through: :books
end

Diagrama de todos los modelos de la librería

2 Recuperación de Objetos de la Base de Datos

Para recuperar objetos de la base de datos, Active Record proporciona varios métodos de búsqueda. Cada método de búsqueda te permite pasar argumentos para realizar ciertas consultas en tu base de datos sin escribir SQL en bruto.

Los métodos son:

Los métodos de búsqueda que devuelven una colección, como where y group, devuelven una instancia de ActiveRecord::Relation. Los métodos que encuentran una sola entidad, como find y first, devuelven una única instancia del modelo.

La operación principal de Model.find(options) se puede resumir como:

  • Convertir las opciones suministradas en una consulta SQL equivalente.
  • Ejecutar la consulta SQL y recuperar los resultados correspondientes de la base de datos.
  • Instanciar el objeto Ruby equivalente del modelo apropiado para cada fila resultante.
  • Ejecutar after_find y luego las devoluciones de llamada after_initialize, si las hay.

2.1 Recuperación de un Solo Objeto

Active Record proporciona varias formas diferentes de recuperar un solo objeto.

2.1.1 find

Usando el método find puedes recuperar el objeto correspondiente a la clave primaria especificada que coincida con cualquier opción suministrada. Por ejemplo:

# Encuentra el cliente con clave primaria (id) 10.
irb> customer = Customer.find(10)
=> #<Customer id: 10, first_name: "Ryan">

El equivalente SQL de lo anterior es:

SELECT * FROM customers WHERE (customers.id = 10) LIMIT 1

El método find generará una excepción ActiveRecord::RecordNotFound si no se encuentra un registro coincidente.

También puedes usar este método para consultar múltiples objetos. Llama al método find y pasa un array de claves primarias. La devolución será un array que contiene todos los registros coincidentes para las claves primarias suministradas. Por ejemplo:

# Encuentra los clientes con claves primarias 1 y 10.
irb> customers = Customer.find([1, 10]) # O Customer.find(1, 10)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 10, first_name: "Ryan">]

El equivalente SQL de lo anterior es:

SELECT * FROM customers WHERE (customers.id IN (1,10))

ADVERTENCIA: El método find generará una excepción ActiveRecord::RecordNotFound a menos que se encuentre un registro coincidente para todas las claves primarias suministradas.

Si tu tabla usa una clave primaria compuesta, necesitarás pasar un array a find para encontrar un solo elemento. Por ejemplo, si los clientes se definieron con [:store_id, :id] como clave primaria:

# Encuentra el cliente con store_id 3 y id 17
irb> customers = Customer.find([3, 17])
=> #<Customer store_id: 3, id: 17, first_name: "Magda">

El equivalente SQL de lo anterior es:

SELECT * FROM customers WHERE store_id = 3 AND id = 17

Para encontrar múltiples clientes con IDs compuestos, pasarías un array de arrays:

# Encuentra los clientes con claves primarias [1, 8] y [7, 15].
irb> customers = Customer.find([[1, 8], [7, 15]]) # O Customer.find([1, 8], [7, 15])
=> [#<Customer store_id: 1, id: 8, first_name: "Pat">, #<Customer store_id: 7, id: 15, first_name: "Chris">]

El equivalente SQL de lo anterior es:

SELECT * FROM customers WHERE (store_id = 1 AND id = 8 OR store_id = 7 AND id = 15)

2.1.2 take

El método take recupera un registro sin ningún orden implícito. Por ejemplo:

irb> customer = Customer.take
=> #<Customer id: 1, first_name: "Lifo">

El equivalente SQL de lo anterior es:

SELECT * FROM customers LIMIT 1

El método take devuelve nil si no se encuentra ningún registro y no se generará ninguna excepción.

Puedes pasar un argumento numérico al método take para devolver hasta ese número de resultados. Por ejemplo

irb> customers = Customer.take(2)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 220, first_name: "Sara">]

El equivalente SQL de lo anterior es:

SELECT * FROM customers LIMIT 2

El método take! se comporta exactamente como take, excepto que generará ActiveRecord::RecordNotFound si no se encuentra un registro coincidente.

CONSEJO: El registro recuperado puede variar dependiendo del motor de la base de datos.

2.1.3 first

El método first encuentra el primer registro ordenado por clave primaria (por defecto). Por ejemplo:

irb> customer = Customer.first
=> #<Customer id: 1, first_name: "Lifo">

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 1

El método first devuelve nil si no se encuentra un registro coincidente y no se generará ninguna excepción.

Si tu alcance predeterminado contiene un método de orden, first devolverá el primer registro según este ordenamiento.

Puedes pasar un argumento numérico al método first para devolver hasta ese número de resultados. Por ejemplo

irb> customers = Customer.first(3)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 2, first_name: "Fifo">, #<Customer id: 3, first_name: "Filo">]

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.id ASC LIMIT 3

Los modelos con claves primarias compuestas usarán la clave primaria compuesta completa para el ordenamiento. Por ejemplo, si los clientes se definieron con [:store_id, :id] como clave primaria:

irb> customer = Customer.first
=> #<Customer id: 2, store_id: 1, first_name: "Lifo">

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.store_id ASC, customers.id ASC LIMIT 1

En una colección que está ordenada usando order, first devolverá el primer registro ordenado por el atributo especificado para order.

irb> customer = Customer.order(:first_name).first
=> #<Customer id: 2, first_name: "Fifo">

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.first_name ASC LIMIT 1

El método first! se comporta exactamente como first, excepto que generará ActiveRecord::RecordNotFound si no se encuentra un registro coincidente.

2.1.4 last

El método last encuentra el último registro ordenado por clave primaria (por defecto). Por ejemplo:

irb> customer = Customer.last
=> #<Customer id: 221, first_name: "Russel">

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 1

El método last devuelve nil si no se encuentra un registro coincidente y no se generará ninguna excepción.

Los modelos con claves primarias compuestas usarán la clave primaria compuesta completa para el ordenamiento. Por ejemplo, si los clientes se definieron con [:store_id, :id] como clave primaria:

irb> customer = Customer.last
=> #<Customer id: 221, store_id: 1, first_name: "Lifo">

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.store_id DESC, customers.id DESC LIMIT 1

Si tu alcance predeterminado contiene un método de orden, last devolverá el último registro según este ordenamiento.

Puedes pasar un argumento numérico al método last para devolver hasta ese número de resultados. Por ejemplo

irb> customers = Customer.last(3)
=> [#<Customer id: 219, first_name: "James">, #<Customer id: 220, first_name: "Sara">, #<Customer id: 221, first_name: "Russel">]

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.id DESC LIMIT 3

En una colección que está ordenada usando order, last devolverá el último registro ordenado por el atributo especificado para order.

irb> customer = Customer.order(:first_name).last
=> #<Customer id: 220, first_name: "Sara">

El equivalente SQL de lo anterior es:

SELECT * FROM customers ORDER BY customers.first_name DESC LIMIT 1

El método last! se comporta exactamente como last, excepto que generará ActiveRecord::RecordNotFound si no se encuentra un registro coincidente.

2.1.5 find_by

El método find_by encuentra el primer registro que coincide con algunas condiciones. Por ejemplo:

irb> Customer.find_by first_name: 'Lifo'
=> #<Customer id: 1, first_name: "Lifo">

irb> Customer.find_by first_name: 'Jon'
=> nil

Es equivalente a escribir:

Customer.where(first_name: 'Lifo').take

El equivalente SQL de lo anterior es:

SELECT * FROM customers WHERE (customers.first_name = 'Lifo') LIMIT 1

Ten en cuenta que no hay ORDER BY en el SQL anterior. Si tus condiciones de find_by pueden coincidir con múltiples registros, deberías aplicar un orden para garantizar un resultado determinista.

El método find_by! se comporta exactamente como find_by, excepto que generará ActiveRecord::RecordNotFound si no se encuentra un registro coincidente. Por ejemplo:

irb> Customer.find_by! first_name: 'does not exist'
ActiveRecord::RecordNotFound

Esto es equivalente a escribir:

Customer.where(first_name: 'does not exist').take!
2.1.5.1 Condiciones con :id

Al especificar condiciones en métodos como find_by y where, el uso de id coincidirá con un atributo :id en el modelo. Esto es diferente de find, donde el ID pasado debe ser un valor de clave primaria.

Ten precaución al usar find_by(id:) en modelos donde :id no es la clave primaria, como modelos de clave primaria compuesta. Por ejemplo, si los clientes se definieron con [:store_id, :id] como clave primaria:

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id) # Customer.find_by(id: [5, 10])
=> #<Customer id: 5, store_id: 3, first_name: "Bob">

Aquí, podríamos intentar buscar un solo registro con la clave primaria compuesta [5, 10], pero Active Record buscará un registro con una columna :id de ya sea 5 o 10, y puede devolver el registro incorrecto.

CONSEJO: El método id_value se puede usar para obtener el valor de la columna :id para un registro, para su uso en métodos de búsqueda como find_by y where. Ver ejemplo a continuación:

irb> customer = Customer.last
=> #<Customer id: 10, store_id: 5, first_name: "Joe">
irb> Customer.find_by(id: customer.id_value) # Customer.find_by(id: 10)
=> #<Customer id: 10, store_id: 5, first_name: "Joe">

2.2 Recuperación de Múltiples Objetos en Lotes

A menudo necesitamos iterar sobre un gran conjunto de registros, como cuando enviamos un boletín a un gran conjunto de clientes, o cuando exportamos datos.

Esto puede parecer sencillo:

# Esto puede consumir demasiada memoria si la tabla es grande.
Customer.all.each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

Pero este enfoque se vuelve cada vez más impráctico a medida que aumenta el tamaño de la tabla, ya que Customer.all.each instruye a Active Record a recuperar toda la tabla en una sola pasada, construir un objeto de modelo por fila y luego mantener todo el array de objetos de modelo en memoria. De hecho, si tenemos un gran número de registros, toda la colección puede exceder la cantidad de memoria disponible.

Rails proporciona dos métodos que abordan este problema dividiendo los registros en lotes amigables con la memoria para su procesamiento. El primer método, find_each, recupera un lote de registros y luego cede cada registro al bloque individualmente como un modelo. El segundo método, find_in_batches, recupera un lote de registros y luego cede todo el lote al bloque como un array de modelos.

CONSEJO: Los métodos find_each y find_in_batches están destinados a ser utilizados en el procesamiento por lotes de un gran número de registros que no cabrían en memoria a la vez. Si solo necesitas recorrer mil registros, los métodos de búsqueda regulares son la opción preferida.

2.2.1 find_each

El método find_each recupera registros en lotes y luego cede cada uno al bloque. En el siguiente ejemplo, find_each recupera clientes en lotes de 1000 y los cede al bloque uno por uno:

Customer.find_each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

Este proceso se repite, recuperando más lotes según sea necesario, hasta que todos los registros hayan sido procesados.

find_each funciona en clases de modelo, como se ve arriba, y también en relaciones:

Customer.where(weekly_subscriber: true).find_each do |customer|
  NewsMailer.weekly(customer).deliver_now
end

siempre que no tengan ordenación, ya que el método necesita forzar un orden internamente para iterar.

Si hay un orden presente en el receptor, el comportamiento depende de la bandera config.active_record.error_on_ignored_order. Si es verdadero, se genera ArgumentError, de lo contrario, el orden se ignora y se emite una advertencia, que es el valor predeterminado. Esto se puede anular con la opción :error_on_ignore, explicada a continuación.

2.2.1.1 Opciones para find_each

:batch_size

La opción :batch_size te permite especificar el número de registros que se recuperarán en cada lote, antes de ser pasados individualmente al bloque. Por ejemplo, para recuperar registros en lotes de 5000:

Customer.find_each(batch_size: 5000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

:start

Por defecto, los registros se recuperan en orden ascendente de la clave primaria. La opción :start te permite configurar el primer ID de la secuencia siempre que el ID más bajo no sea el que necesitas. Esto sería útil, por ejemplo, si quisieras reanudar un proceso por lotes interrumpido, siempre que hayas guardado el último ID procesado como punto de control.

Por ejemplo, para enviar boletines solo a clientes con la clave primaria a partir de 2000:

Customer.find_each(start: 2000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

:finish

Similar a la opción :start, :finish te permite configurar el último ID de la secuencia siempre que el ID más alto no sea el que necesitas. Esto sería útil, por ejemplo, si quisieras ejecutar un proceso por lotes utilizando un subconjunto de registros basado en :start y :finish.

Por ejemplo, para enviar boletines solo a clientes con la clave primaria a partir de 2000 hasta 10000:

Customer.find_each(start: 2000, finish: 10000) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

Otro ejemplo sería si quisieras que múltiples trabajadores manejen la misma cola de procesamiento. Podrías hacer que cada trabajador maneje 10000 registros configurando las opciones :start y :finish apropiadas en cada trabajador.

:error_on_ignore

Anula la configuración de la aplicación para especificar si se debe generar un error cuando hay un orden presente en la relación.

:order

Especifica el orden de la clave primaria (puede ser :asc o :desc). El valor predeterminado es :asc.

Customer.find_each(order: :desc) do |customer|
  NewsMailer.weekly(customer).deliver_now
end

2.2.2 find_in_batches

El método find_in_batches es similar a find_each, ya que ambos recuperan lotes de registros. La diferencia es que find_in_batches cede lotes al bloque como un array de modelos, en lugar de individualmente. El siguiente ejemplo cederá al bloque proporcionado un array de hasta 1000 clientes a la vez, con el bloque final conteniendo cualquier cliente restante:

# Da add_customers un array de 1000 clientes a la vez.
Customer.find_in_batches do |customers|
  export.add_customers(customers)
end

find_in_batches funciona en clases de modelo, como se ve arriba, y también en relaciones:

# Da add_customers un array de 1000 clientes recientemente activos a la vez.
Customer.recently_active.find_in_batches do |customers|
  export.add_customers(customers)
end

siempre que no tengan ordenación, ya que el método necesita forzar un orden internamente para iterar.

2.2.2.1 Opciones para find_in_batches

El método find_in_batches acepta las mismas opciones que find_each:

:batch_size

Al igual que para find_each, batch_size establece cuántos registros se recuperarán en cada grupo. Por ejemplo, recuperar lotes de 2500 registros se puede especificar como:

Customer.find_in_batches(batch_size: 2500) do |customers|
  export.add_customers(customers)
end

:start

La opción start permite especificar el ID inicial desde donde se seleccionarán los registros. Como se mencionó antes, por defecto los registros se recuperan en orden ascendente de la clave primaria. Por ejemplo, para recuperar clientes comenzando en ID: 5000 en lotes de 2500 registros, se puede usar el siguiente código:

Customer.find_in_batches(batch_size: 2500, start: 5000) do |customers|
  export.add_customers(customers)
end

:finish

La opción finish permite especificar el ID final de los registros a recuperar. El código a continuación muestra el caso de recuperar clientes en lotes, hasta el cliente con ID: 7000:

Customer.find_in_batches(finish: 7000) do |customers|
  export.add_customers(customers)
end

:error_on_ignore

La opción error_on_ignore anula la configuración de la aplicación para especificar si se debe generar un error cuando hay un orden específico presente en la relación.

3 Condiciones

El método where te permite especificar condiciones para limitar los registros devueltos, representando la parte WHERE de la declaración SQL. Las condiciones se pueden especificar como una cadena, un array o un hash.

3.1 Condiciones de Cadena Pura

Si deseas agregar condiciones a tu búsqueda, podrías simplemente especificarlas allí, como Book.where("title = 'Introduction to Algorithms'"). Esto encontrará todos los libros donde el valor del campo title sea 'Introduction to Algorithms'.

ADVERTENCIA: Construir tus propias condiciones como cadenas puras puede dejarte vulnerable a exploits de inyección SQL. Por ejemplo, Book.where("title LIKE '%#{params[:title]}%'") no es seguro. Consulta la siguiente sección para conocer la forma preferida de manejar condiciones usando un array.

3.2 Condiciones de Array

Ahora, ¿qué pasa si ese título podría variar, digamos como un argumento de algún lugar? La búsqueda entonces tomaría la forma:

Book.where("title = ?", params[:title])

Active Record tomará el primer argumento como la cadena de condiciones y cualquier argumento adicional reemplazará los signos de interrogación (?) en ella.

Si deseas especificar múltiples condiciones:

Book.where("title = ? AND out_of_print = ?", params[:title], false)

En este ejemplo, el primer signo de interrogación será reemplazado con el valor en params[:title] y el segundo será reemplazado con la representación SQL de false, que depende del adaptador.

Este código es altamente preferible:

Book.where("title = ?", params[:title])

a este código:

Book.where("title = #{params[:title]}")

debido a la seguridad de los argumentos. Poner la variable directamente en la cadena de condiciones pasará la variable a la base de datos tal cual. Esto significa que será una variable no escapada directamente de un usuario que podría tener intenciones maliciosas. Si haces esto, pones en riesgo toda tu base de datos porque una vez que un usuario descubre que puede explotar tu base de datos, puede hacer casi cualquier cosa con ella. Nunca pongas tus argumentos directamente dentro de la cadena de condiciones.

CONSEJO: Para obtener más información sobre los peligros de la inyección SQL, consulta la Guía de Seguridad de Ruby on Rails.

3.2.1 Condiciones de Marcadores de Posición

Similar al estilo de reemplazo (?) de params, también puedes especificar claves en tu cadena de condiciones junto con un hash de claves/valores correspondiente:

Book.where("created_at >= :start_date AND created_at <= :end_date",
  { start_date: params[:start_date], end_date: params[:end_date] })

Esto hace que la legibilidad sea más clara si tienes un gran número de condiciones variables.

3.2.2 Condiciones que Usan LIKE

Aunque los argumentos de condición se escapan automáticamente para prevenir inyecciones SQL, los comodines SQL LIKE (es decir, % y _) no se escapan. Esto puede causar un comportamiento inesperado si se utiliza un valor no sanitizado en un argumento. Por ejemplo:

Book.where("title LIKE ?", params[:title] + "%")

En el código anterior, la intención es coincidir con títulos que comienzan con una cadena especificada por el usuario. Sin embargo, cualquier aparición de % o _ en params[:title] será tratada como comodines, lo que lleva a resultados de consulta sorprendentes. En algunas circunstancias, esto también puede prevenir que la base de datos use un índice previsto, lo que lleva a una consulta mucho más lenta.

Para evitar estos problemas, utiliza sanitize_sql_like para escapar los caracteres comodín en la parte relevante del argumento:

Book.where("title LIKE ?",
  Book.sanitize_sql_like(params[:title]) + "%")

3.3 Condiciones de Hash

Active Record también te permite pasar condiciones en hash, lo que puede aumentar la legibilidad de tu sintaxis de condiciones. Con condiciones de hash, pasas un hash con claves de los campos que deseas calificar y los valores de cómo deseas calificarlos:

NOTA: Solo son posibles la igualdad, el rango y la verificación de subconjuntos con condiciones de Hash.

3.3.1 Condiciones de Igualdad

Book.where(out_of_print: true)

Esto generará SQL como este:

SELECT * FROM books WHERE (books.out_of_print = 1)

El nombre del campo también puede ser una cadena:

Book.where('out_of_print' => true)

En el caso de una relación belongs_to, se puede usar una clave de asociación para especificar el modelo si se utiliza un objeto de Active Record como valor. Este método también funciona con relaciones polimórficas.

author = Author.first
Book.where(author: author)
Author.joins(:books).where(books: { author: author })

Las condiciones de hash también se pueden especificar en una sintaxis similar a una tupla, donde la clave es un array de columnas y el valor es un array de tuplas:

Book.where([:author_id, :id] => [[15, 1], [15, 2]])

Esta sintaxis puede ser útil para consultar relaciones donde la tabla usa una clave primaria compuesta:

class Book < ApplicationRecord
  self.primary_key = [:author_id, :id]
end

Book.where(Book.primary_key => [[2, 1], [3, 1]])

3.3.2 Condiciones de Rango

Book.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)

Esto encontrará todos los libros creados ayer usando una declaración SQL BETWEEN:

SELECT * FROM books WHERE (books.created_at BETWEEN '2008-12-21 00:00:00' AND '2008-12-22 00:00:00')

Esto demuestra una sintaxis más corta para los ejemplos en Condiciones de Array

Los rangos sin inicio y sin fin son compatibles y se pueden usar para construir condiciones de menor/mayor que.

Book.where(created_at: (Time.now.midnight - 1.day)..)

Esto generaría SQL como:

SELECT * FROM books WHERE books.created_at >= '2008-12-21 00:00:00'

3.3.3 Condiciones de Subconjunto

Si deseas encontrar registros usando la expresión IN, puedes pasar un array al hash de condiciones:

Customer.where(orders_count: [1, 3, 5])

Este código generará SQL como este:

SELECT * FROM customers WHERE (customers.orders_count IN (1,3,5))

3.4 Condiciones NOT

Las consultas SQL NOT se pueden construir con where.not:

Customer.where.not(orders_count: [1, 3, 5])

En otras palabras, esta consulta se puede generar llamando a where sin argumento, luego encadenando inmediatamente con not pasando condiciones de where. Esto generará SQL como este:

SELECT * FROM customers WHERE (customers.orders_count NOT IN (1,3,5))

Si una consulta tiene una condición de hash con valores no nulos en una columna nullable, los registros que tienen valores nil en la columna nullable no serán devueltos. Por ejemplo:

Customer.create!(nullable_country: nil)
Customer.where.not(nullable_country: "UK")
# => []

# Pero
Customer.create!(nullable_country: "UK")
Customer.where.not(nullable_country: nil)
# => [#<Customer id: 2, nullable_country: "UK">]

3.5 Condiciones OR

Las condiciones OR entre dos relaciones se pueden construir llamando a or en la primera relación, y pasando la segunda como argumento.

Customer.where(last_name: 'Smith').or(Customer.where(orders_count: [1, 3, 5]))
SELECT * FROM customers WHERE (customers.last_name = 'Smith' OR customers.orders_count IN (1,3,5))

3.6 Condiciones AND

Las condiciones AND se pueden construir encadenando condiciones where.

Customer.where(last_name: 'Smith').where(orders_count: [1, 3, 5])
SELECT * FROM customers WHERE customers.last_name = 'Smith' AND customers.orders_count IN (1,3,5)

Las condiciones AND para la intersección lógica entre relaciones se pueden construir llamando a and en la primera relación, y pasando la segunda como argumento.

Customer.where(id: [1, 2]).and(Customer.where(id: [2, 3]))
SELECT * FROM customers WHERE (customers.id IN (1, 2) AND customers.id IN (2, 3))

4 Ordenamiento

Para recuperar registros de la base de datos en un orden específico, puedes usar el método order.

Por ejemplo, si estás obteniendo un conjunto de registros y deseas ordenarlos en orden ascendente por el campo created_at en tu tabla:

Book.order(:created_at)
# O
Book.order("created_at")

También podrías especificar ASC o DESC:

Book.order(created_at: :desc)
# O
Book.order(created_at: :asc)
# O
Book.order("created_at DESC")
# O
Book.order("created_at ASC")

O ordenar por múltiples campos:

Book.order(title: :asc, created_at: :desc)
# O
Book.order(:title, created_at: :desc)
# O
Book.order("title ASC, created_at DESC")
# O
Book.order("title ASC", "created_at DESC")

Si deseas llamar a order varias veces, los órdenes subsiguientes se agregarán al primero:

irb> Book.order("title ASC").order("created_at DESC")
SELECT * FROM books ORDER BY title ASC, created_at DESC

También puedes ordenar desde una tabla unida

Book.includes(:author).order(books: { print_year: :desc }, authors: { name: :asc })
# O
Book.includes(:author).order('books.print_year desc', 'authors.name asc')

ADVERTENCIA: En la mayoría de los sistemas de bases de datos, al seleccionar campos con distinct de un conjunto de resultados usando métodos como select, pluck e ids; el método order generará una excepción ActiveRecord::StatementInvalid a menos que el/los campo(s) utilizado(s) en la cláusula order estén incluidos en la lista de selección. Consulta la siguiente sección para seleccionar campos del conjunto de resultados.

5 Seleccionar Campos Específicos

Por defecto, Model.find selecciona todos los campos del conjunto de resultados usando select *.

Para seleccionar solo un subconjunto de campos del conjunto de resultados, puedes especificar el subconjunto a través del método select.

Por ejemplo, para seleccionar solo las columnas isbn y out_of_print:

Book.select(:isbn, :out_of_print)
# O
Book.select("isbn, out_of_print")

La consulta SQL utilizada por esta llamada a find será algo como:

SELECT isbn, out_of_print FROM books

Ten cuidado porque esto también significa que estás inicializando un objeto de modelo con solo los campos que has seleccionado. Si intentas acceder a un campo que no está en el registro inicializado, recibirás:

ActiveModel::MissingAttributeError: missing attribute '<attribute>' for Book

Donde <attribute> es el atributo que solicitaste. El método id no generará el ActiveRecord::MissingAttributeError, así que ten cuidado al trabajar con asociaciones porque necesitan el método id para funcionar correctamente.

Si deseas obtener solo un registro por valor único en un determinado campo, puedes usar distinct:

Customer.select(:last_name).distinct

Esto generaría SQL como:

SELECT DISTINCT last_name FROM customers

También puedes eliminar la restricción de unicidad:

# Devuelve apellidos únicos
query = Customer.select(:last_name).distinct

# Devuelve todos los apellidos, incluso si hay duplicados
query.distinct(false)

6 Límite y Desplazamiento

Para aplicar LIMIT al SQL ejecutado por el Model.find, puedes especificar el LIMIT usando los métodos limit y offset en la relación.

Puedes usar limit para especificar el número de registros a recuperar, y usar offset para especificar el número de registros a omitir antes de comenzar a devolver los registros. Por ejemplo

Customer.limit(5)

devolverá un máximo de 5 clientes y, como no especifica ningún desplazamiento, devolverá los primeros 5 en la tabla. El SQL que ejecuta se ve así:

SELECT * FROM customers LIMIT 5

Agregando offset a eso

Customer.limit(5).offset(30)

devolverá en su lugar un máximo de 5 clientes comenzando con el 31º. El SQL se ve así:

SELECT * FROM customers LIMIT 5 OFFSET 30

7 Agrupación

Para aplicar una cláusula GROUP BY al SQL ejecutado por el buscador, puedes usar el método group.

Por ejemplo, si deseas encontrar una colección de las fechas en las que se crearon pedidos:

Order.select("created_at").group("created_at")

Y esto te dará un solo objeto Order para cada fecha donde hay pedidos en la base de datos.

El SQL que se ejecutaría sería algo como esto:

SELECT created_at
FROM orders
GROUP BY created_at

7.1 Total de Elementos Agrupados

Para obtener el total de elementos agrupados en una sola consulta, llama a count después del group.

irb> Order.group(:status).count
=> {"being_packed"=>7, "shipped"=>12}

El SQL que se ejecutaría sería algo como esto:

SELECT COUNT (*) AS count_all, status AS status
FROM orders
GROUP BY status

7.2 Condiciones HAVING

SQL utiliza la cláusula HAVING para especificar condiciones en los campos GROUP BY. Puedes agregar la cláusula HAVING al SQL ejecutado por el Model.find agregando el método having al find.

Por ejemplo:

Order.select("created_at as ordered_date, sum(total) as total_price").
  group("created_at").having("sum(total) > ?", 200)

El SQL que se ejecutaría sería algo como esto:

SELECT created_at as ordered_date, sum(total) as total_price
FROM orders
GROUP BY created_at
HAVING sum(total) > 200

Esto devuelve la fecha y el precio total para cada objeto de pedido, agrupados por el día en que se ordenaron y donde el total es más de $200.

Accederías al total_price para cada objeto de pedido devuelto así:

big_orders = Order.select("created_at, sum(total) as total_price")
                  .group("created_at")
                  .having("sum(total) > ?", 200)

big_orders[0].total_price
# Devuelve el precio total para el primer objeto de Order

8 Sobrescribir Condiciones

8.1 unscope

Puedes especificar ciertas condiciones para ser eliminadas usando el método unscope. Por ejemplo:

Book.where('id > 100').limit(20).order('id desc').unscope(:order)

El SQL que se ejecutaría:

SELECT * FROM books WHERE id > 100 LIMIT 20

-- Consulta original sin `unscope`
SELECT * FROM books WHERE id > 100 ORDER BY id desc LIMIT 20

También puedes eliminar condiciones específicas de where. Por ejemplo, esto eliminará la condición id de la cláusula where:

Book.where(id: 10, out_of_print: false).unscope(where: :id)
# SELECT books.* FROM books WHERE out_of_print = 0

Una relación que ha usado unscope afectará cualquier relación en la que se fusione:

Book.order('id desc').merge(Book.unscope(:order))
# SELECT books.* FROM books

8.2 only

También puedes sobrescribir condiciones usando el método only. Por ejemplo:

Book.where('id > 10').limit(20).order('id desc').only(:order, :where)

El SQL que se ejecutaría:

SELECT * FROM books WHERE id > 10 ORDER BY id DESC

-- Consulta original sin `only`
SELECT * FROM books WHERE id > 10 ORDER BY id DESC LIMIT 20

8.3 reselect

El método reselect sobrescribe una declaración select existente. Por ejemplo:

Book.select(:title, :isbn).reselect(:created_at)

El SQL que se ejecutaría:

SELECT books.created_at FROM books

Compara esto con el caso donde no se usa la cláusula reselect:

Book.select(:title, :isbn).select(:created_at)

el SQL ejecutado sería:

SELECT books.title, books.isbn, books.created_at FROM books

8.4 reorder

El método reorder sobrescribe el orden del alcance predeterminado. Por ejemplo, si la definición de clase incluye esto:

class Author < ApplicationRecord
  has_many :books, -> { order(year_published: :desc) }
end

Y ejecutas esto:

Author.find(10).books

El SQL que se ejecutaría:

SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published DESC

Puedes usar la cláusula reorder para especificar una forma diferente de ordenar los libros:

Author.find(10).books.reorder('year_published ASC')

El SQL que se ejecutaría:

SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published ASC

8.5 reverse_order

El método reverse_order invierte la cláusula de ordenamiento si está especificada.

Book.where("author_id > 10").order(:year_published).reverse_order

El SQL que se ejecutaría:

SELECT * FROM books WHERE author_id > 10 ORDER BY year_published DESC

Si no se especifica ninguna cláusula de orden en la consulta, el reverse_order ordena por la clave primaria en orden inverso.

Book.where("author_id > 10").reverse_order

El SQL que se ejecutaría:

SELECT * FROM books WHERE author_id > 10 ORDER BY books.id DESC

El método reverse_order no acepta ningún argumento.

8.6 rewhere

El método rewhere sobrescribe una condición where existente y nombrada. Por ejemplo:

Book.where(out_of_print: true).rewhere(out_of_print: false)

El SQL que se ejecutaría:

SELECT * FROM books WHERE out_of_print = 0

Si no se usa la cláusula rewhere, las cláusulas where se combinan con AND:

Book.where(out_of_print: true).where(out_of_print: false)

el SQL ejecutado sería:

SELECT * FROM books WHERE out_of_print = 1 AND out_of_print = 0

8.7 regroup

El método regroup sobrescribe una condición group existente y nombrada. Por ejemplo:

Book.group(:author).regroup(:id)

El SQL que se ejecutaría:

SELECT * FROM books GROUP BY id

Si no se usa la cláusula regroup, las cláusulas de grupo se combinan juntas:

Book.group(:author).group(:id)

el SQL ejecutado sería:

SELECT * FROM books GROUP BY author, id

9 Relación Nula

El método none devuelve una relación encadenable sin registros. Cualquier condición subsiguiente encadenada a la relación devuelta continuará generando relaciones vacías. Esto es útil en escenarios donde necesitas una respuesta encadenable a un método o un alcance que podría devolver cero resultados.

Book.none # devuelve una Relación vacía y no ejecuta consultas.
# El método highlighted_reviews a continuación se espera que siempre devuelva una Relación.
Book.first.highlighted_reviews.average(:rating)
# => Devuelve la calificación promedio de un libro

class Book
  # Devuelve reseñas si hay al menos 5,
  # de lo contrario considera esto como un libro no revisado
  def highlighted_reviews
    if reviews.count > 5
      reviews
    else
      Review.none # Aún no cumple con el umbral mínimo
    end
  end
end

10 Objetos de Solo Lectura

Active Record proporciona el método readonly en una relación para desautorizar explícitamente la modificación de cualquiera de los objetos devueltos. Cualquier intento de alterar un registro de solo lectura no tendrá éxito, generando una excepción ActiveRecord::ReadOnlyRecord.

customer = Customer.readonly.first
customer.visits += 1
customer.save # Genera una excepción ActiveRecord::ReadOnlyRecord

Como customer se establece explícitamente como un objeto de solo lectura, el código anterior generará una excepción ActiveRecord::ReadOnlyRecord al llamar a customer.save con un valor actualizado de visits.

11 Bloqueo de Registros para Actualización

El bloqueo es útil para prevenir condiciones de carrera al actualizar registros en la base de datos y asegurar actualizaciones atómicas.

Active Record proporciona dos mecanismos de bloqueo:

  • Bloqueo Optimista
  • Bloqueo Pesimista

11.1 Bloqueo Optimista

El bloqueo optimista permite que múltiples usuarios accedan al mismo registro para ediciones, y asume un mínimo de conflictos con los datos. Lo hace verificando si otro proceso ha realizado cambios en un registro desde que se abrió. Se genera una excepción ActiveRecord::StaleObjectError si eso ha ocurrido y la solicitud de actualización se ignora.

Columna de bloqueo optimista

Para usar el bloqueo optimista, la tabla necesita tener una columna llamada lock_version de tipo entero. Cada vez que se actualiza el registro, Active Record incrementa la columna lock_version. Si se realiza una solicitud de actualización con un valor más bajo en el campo lock_version que el que está actualmente en la columna lock_version en la base de datos, la solicitud de actualización fallará con una excepción ActiveRecord::StaleObjectError.

Por ejemplo:

c1 = Customer.find(1)
c2 = Customer.find(1)

c1.first_name = "Sandra"
c1.save

c2.first_name = "Michael"
c2.save # Genera una excepción ActiveRecord::StaleObjectError

Eres entonces responsable de lidiar con el conflicto rescatando la excepción y ya sea retrocediendo, fusionando o aplicando la lógica de negocio necesaria para resolver el conflicto.

Este comportamiento se puede desactivar configurando ActiveRecord::Base.lock_optimistically = false.

Para sobrescribir el nombre de la columna lock_version, ActiveRecord::Base proporciona un atributo de clase llamado locking_column:

class Customer < ApplicationRecord
  self.locking_column = :lock_customer_column
end

11.2 Bloqueo Pesimista

El bloqueo pesimista utiliza un mecanismo de bloqueo proporcionado por la base de datos subyacente. Usar lock al construir una relación obtiene un bloqueo exclusivo en las filas seleccionadas. Las relaciones que usan lock generalmente se envuelven dentro de una transacción para prevenir condiciones de deadlock.

Por ejemplo:

Book.transaction do
  book = Book.lock.first
  book.title = 'Algorithms, second edition'
  book.save!
end

La sesión anterior produce el siguiente SQL para un backend MySQL:

SQL (0.2ms)   BEGIN
Book Load (0.3ms)   SELECT * FROM books LIMIT 1 FOR UPDATE
Book Update (0.4ms)   UPDATE books SET updated_at = '2009-02-07 18:05:56', title = 'Algorithms, second edition' WHERE id = 1
SQL (0.8ms)   COMMIT

También puedes pasar SQL en bruto al método lock para permitir diferentes tipos de bloqueos. Por ejemplo, MySQL tiene una expresión llamada LOCK IN SHARE MODE donde puedes bloquear un registro pero aún permitir que otras consultas lo lean. Para especificar esta expresión, simplemente pásala como la opción de bloqueo:

Book.transaction do
  book = Book.lock("LOCK IN SHARE MODE").find(1)
  book.increment!(:views)
end

NOTA: Ten en cuenta que tu base de datos debe soportar el SQL en bruto que pases al método lock.

Si ya tienes una instancia de tu modelo, puedes iniciar una transacción y adquirir el bloqueo de una sola vez usando el siguiente código:

book = Book.first
book.with_lock do
  # Este bloque se llama dentro de una transacción,
  # book ya está bloqueado.
  book.increment!(:views)
end

12 Unión de Tablas

Active Record proporciona dos métodos de búsqueda para especificar cláusulas JOIN en el SQL resultante: joins y left_outer_joins. Mientras que joins se debe usar para INNER JOIN o consultas personalizadas, left_outer_joins se usa para consultas que usan LEFT OUTER JOIN.

12.1 joins

Hay múltiples formas de usar el método joins.

12.1.1 Usando un Fragmento SQL de Cadena

Puedes simplemente suministrar el SQL en bruto especificando la cláusula JOIN a joins:

Author.joins("INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE")

Esto resultará en el siguiente SQL:

SELECT authors.* FROM authors INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE

12.1.2 Usando Array/Hash de Asociaciones Nombradas

Active Record te permite usar los nombres de las asociaciones definidas en el modelo como un atajo para especificar cláusulas JOIN para esas asociaciones al usar el método joins.

Todo lo siguiente producirá las consultas de unión esperadas usando INNER JOIN:

12.1.2.1 Uniendo una Sola Asociación
Book.joins(:reviews)

Esto produce:

SELECT books.* FROM books
  INNER JOIN reviews ON reviews.book_id = books.id

O, en inglés: "devuelve un objeto Book para todos los libros con reseñas". Ten en cuenta que verás libros duplicados si un libro tiene más de una reseña. Si deseas libros únicos, puedes usar Book.joins(:reviews).distinct.

12.1.3 Uniendo Múltiples Asociaciones

Book.joins(:author, :reviews)

Esto produce:

SELECT books.* FROM books
  INNER JOIN authors ON authors.id = books.author_id
  INNER JOIN reviews ON reviews.book_id = books.id

O, en inglés: "devuelve todos los libros que tienen un autor y al menos una reseña". Ten en cuenta nuevamente que los libros con múltiples reseñas aparecerán múltiples veces.

12.1.3.1 Uniendo Asociaciones Anidadas (Nivel Único)
Book.joins(reviews: :customer)

Esto produce:

SELECT books.* FROM books
  INNER JOIN reviews ON reviews.book_id = books.id
  INNER JOIN customers ON customers.id = reviews.customer_id

O, en inglés: "devuelve todos los libros que tienen una reseña de un cliente."

12.1.3.2 Uniendo Asociaciones Anidadas (Múltiples Niveles)
Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])

Esto produce:

SELECT authors.* FROM authors
  INNER JOIN books ON books.author_id = authors.id
  INNER JOIN reviews ON reviews.book_id = books.id
  INNER JOIN customers ON customers.id = reviews.customer_id
  INNER JOIN orders ON orders.customer_id = customers.id
INNER JOIN suppliers ON suppliers.id = books.supplier_id

O, en inglés: "devuelve todos los autores que tienen libros con reseñas y han sido ordenados por un cliente, y los proveedores para esos libros."

12.1.4 Especificando Condiciones en las Tablas Unidas

Puedes especificar condiciones en las tablas unidas usando las condiciones regulares de Array y Cadena. Las condiciones de Hash proporcionan una sintaxis especial para especificar condiciones para las tablas unidas:

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where('orders.created_at' => time_range).distinct

Esto encontrará todos los clientes que tienen pedidos que fueron creados ayer, usando una expresión SQL BETWEEN para comparar created_at.

Una sintaxis alternativa y más limpia es anidar las condiciones del hash:

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where(orders: { created_at: time_range }).distinct

Para condiciones más avanzadas o para reutilizar un alcance nombrado existente, se puede usar [merge][]. Primero, agreguemos un nuevo alcance nombrado al modelo Order:

class Order < ApplicationRecord
  belongs_to :customer

  scope :created_in_time_range, ->(time_range) {
    where(created_at: time_range)
  }
end

Ahora podemos usar merge para fusionar el alcance created_in_time_range:

time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).merge(Order.created_in_time_range(time_range)).distinct

Esto encontrará todos los clientes que tienen pedidos que fueron creados ayer, nuevamente usando una expresión SQL BETWEEN.

12.2 left_outer_joins

Si deseas seleccionar un conjunto de registros, ya sea que tengan o no registros asociados, puedes usar el método left_outer_joins.

Customer.left_outer_joins(:reviews).distinct.select('customers.*, COUNT(reviews.*) AS reviews_count').group('customers.id')

Lo que produce:

SELECT DISTINCT customers.*, COUNT(reviews.*) AS reviews_count FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id GROUP BY customers.id

Lo que significa: "devuelve todos los clientes con su conteo de reseñas, ya sea que tengan o no alguna reseña".

12.3 where.associated y where.missing

Los métodos de consulta associated y missing te permiten seleccionar un conjunto de registros basado en la presencia o ausencia de una asociación.

Para usar where.associated:

Customer.where.associated(:reviews)

Produce:

SELECT customers.* FROM customers
INNER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NOT NULL

Lo que significa "devuelve todos los clientes que han hecho al menos una reseña".

Para usar where.missing:

Customer.where.missing(:reviews)

Produce:

SELECT customers.* FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NULL

Lo que significa "devuelve todos los clientes que no han hecho ninguna reseña".

13 Carga Anticipada de Asociaciones

La carga anticipada es el mecanismo para cargar los registros asociados de los objetos devueltos por Model.find utilizando la menor cantidad de consultas posible.

13.1 Problema de Consultas N + 1

Considera el siguiente código, que encuentra 10 libros e imprime el apellido de sus autores:

books = Book.limit(10)

books.each do |book|
  puts book.author.last_name
end

Este código parece correcto a primera vista. Pero el problema radica en el número total de consultas ejecutadas. El código anterior ejecuta 1 (para encontrar 10 libros) + 10 (uno por cada libro para cargar el autor) = 11 consultas en total.

13.1.1 Solución al Problema de Consultas N + 1

Active Record te permite especificar de antemano todas las asociaciones que se van a cargar.

Los métodos son:

13.2 includes

Con includes, Active Record asegura que todas las asociaciones especificadas se carguen usando el menor número posible de consultas.

Revisando el caso anterior usando el método includes, podríamos reescribir Book.limit(10) para cargar anticipadamente autores:

books = Book.includes(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

El código anterior ejecutará solo 2 consultas, en lugar de las 11 consultas del caso original:

SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
  WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)

13.2.1 Carga Anticipada de Múltiples Asociaciones

Active Record te permite cargar anticipadamente cualquier número de asociaciones con una sola llamada a Model.find usando un array, hash, o un hash anidado de array/hash con el método includes.

13.2.1.1 Array de Múltiples Asociaciones
Customer.includes(:orders, :reviews)

Esto carga todos los clientes y los pedidos y reseñas asociados para cada uno.

13.2.1.2 Hash de Asociaciones Anidadas
Customer.includes(orders: { books: [:supplier, :author] }).find(1)

Esto encontrará el cliente con id 1 y cargará anticipadamente todos los pedidos asociados para él, los libros para todos los pedidos, y el autor y proveedor para cada uno de los libros.

13.2.2 Especificando Condiciones en Asociaciones Cargadas Anticipadamente

Aunque Active Record te permite especificar condiciones en las asociaciones cargadas anticipadamente al igual que joins, la forma recomendada es usar joins en su lugar.

Sin embargo, si debes hacer esto, puedes usar where como lo harías normalmente.

Author.includes(:books).where(books: { out_of_print: true })

Esto generaría una consulta que contiene un LEFT OUTER JOIN, mientras que el método joins generaría una usando la función INNER JOIN en su lugar.

  SELECT authors.id AS t0_r0, ... books.updated_at AS t1_r5 FROM authors LEFT OUTER JOIN books ON books.author_id = authors.id WHERE (books.out_of_print = 1)

Si no hubiera condición where, esto generaría el conjunto normal de dos consultas.

NOTA: Usar where de esta manera solo funcionará cuando le pases un Hash. Para fragmentos de SQL necesitas usar references para forzar tablas unidas:

Author.includes(:books).where("books.out_of_print = true").references(:books)

Si, en el caso de esta consulta includes, no hubiera libros para ningún autor, todos los autores aún se cargarían. Al usar joins (un INNER JOIN), las condiciones de unión deben coincidir, de lo contrario, no se devolverán registros.

NOTA: Si una asociación se carga anticipadamente como parte de una unión, cualquier campo de una cláusula select personalizada no estará presente en los modelos cargados. Esto se debe a que es ambiguo si deberían aparecer en el registro principal o en el secundario.

13.3 preload

Con preload, Active Record carga cada asociación especificada usando una consulta por asociación.

Revisando el problema de consultas N + 1, podríamos reescribir Book.limit(10) para cargar anticipadamente autores:

books = Book.preload(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

El código anterior ejecutará solo 2 consultas, en lugar de las 11 consultas del caso original:

SELECT books.* FROM books LIMIT 10
SELECT authors.* FROM authors
  WHERE authors.id IN (1,2,3,4,5,6,7,8,9,10)

NOTA: El método preload utiliza un array, hash, o un hash anidado de array/hash de la misma manera que el método includes para cargar cualquier número de asociaciones con una sola llamada a Model.find. Sin embargo, a diferencia del método includes, no es posible especificar condiciones para asociaciones cargadas anticipadamente.

13.4 eager_load

Con eager_load, Active Record carga todas las asociaciones especificadas usando un LEFT OUTER JOIN.

Revisando el caso donde ocurrió N + 1 usando el método eager_load, podríamos reescribir Book.limit(10) a autores:

books = Book.eager_load(:author).limit(10)

books.each do |book|
  puts book.author.last_name
end

El código anterior ejecutará solo 1 consulta, en lugar de las 11 consultas del caso original:

SELECT "books"."id" AS t0_r0, "books"."title" AS t0_r1, ... FROM "books"
  LEFT OUTER JOIN "authors" ON "authors"."id" = "books"."author_id"
  LIMIT 10

NOTA: El método eager_load utiliza un array, hash, o un hash anidado de array/hash de la misma manera que el método includes para cargar cualquier número de asociaciones con una sola llamada a Model.find. Además, al igual que el método includes, puedes especificar condiciones para asociaciones cargadas anticipadamente.

13.5 strict_loading

La carga anticipada puede prevenir consultas N + 1, pero aún podrías estar cargando algunas asociaciones de manera perezosa. Para asegurarte de que no se cargue ninguna asociación de manera perezosa, puedes habilitar strict_loading.

Al habilitar el modo de carga estricta en una relación, se generará un ActiveRecord::StrictLoadingViolationError si el registro intenta cargar de manera perezosa cualquier asociación:

user = User.strict_loading.first
user.address.city # genera un ActiveRecord::StrictLoadingViolationError
user.comments.to_a # genera un ActiveRecord::StrictLoadingViolationError

Para habilitarlo para todas las relaciones, cambia la bandera config.active_record.strict_loading_by_default a true.

Para enviar violaciones al registro en su lugar, cambia config.active_record.action_on_strict_loading_violation a :log.

13.6 strict_loading!

También podemos habilitar la carga estricta en el propio registro llamando a strict_loading!:

user = User.first
user.strict_loading!
user.address.city # genera un ActiveRecord::StrictLoadingViolationError
user.comments.to_a # genera un ActiveRecord::StrictLoadingViolationError

strict_loading! también toma un argumento :mode. Configurarlo en :n_plus_one_only solo generará un error si una asociación que conducirá a una consulta N + 1 se carga de manera perezosa:

user.strict_loading!(mode: :n_plus_one_only)
user.address.city # => "Tatooine"
user.comments.to_a # => [#<Comment:0x00...]
user.comments.first.likes.to_a # genera un ActiveRecord::StrictLoadingViolationError

13.7 Opción strict_loading en una asociación

También podemos habilitar la carga estricta para una sola asociación proporcionando la opción strict_loading:

class Author < ApplicationRecord
  has_many :books, strict_loading: true
end

14 Alcances

El alcance te permite especificar consultas de uso común que se pueden referenciar como llamadas a métodos en los objetos de asociación o modelos. Con estos alcances, puedes usar cada método cubierto anteriormente, como where, joins e includes. Todos los cuerpos de alcance deben devolver un objeto ActiveRecord::Relation o nil para permitir que se llamen más métodos (como otros alcances) en él.

Para definir un alcance simple, usamos el método scope dentro de la clase, pasando la consulta que nos gustaría ejecutar cuando se llame a este alcance:

class Book < ApplicationRecord
  scope :out_of_print, -> { where(out_of_print: true) }
end

Para llamar a este alcance out_of_print podemos llamarlo en la clase:

irb> Book.out_of_print
=> #<ActiveRecord::Relation> # todos los libros agotados

O en una asociación que consiste en objetos Book:

irb> author = Author.first
irb> author.books.out_of_print
=> #<ActiveRecord::Relation> # todos los libros agotados por `author`

Los alcances también son encadenables dentro de los alcances:

class Book < ApplicationRecord
  scope :out_of_print, -> { where(out_of_print: true) }
  scope :out_of_print_and_expensive, -> { out_of_print.where("price > 500") }
end

14.1 Pasar Argumentos

Tu alcance puede tomar argumentos:

class Book < ApplicationRecord
  scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end

Llama al alcance como si fuera un método de clase:

irb> Book.costs_more_than(100.10)

Sin embargo, esto solo está duplicando la funcionalidad que te proporcionaría un método de clase.

class Book < ApplicationRecord
  def self.costs_more_than(amount)
    where("price > ?", amount)
  end
end

Estos métodos seguirán siendo accesibles en los objetos de asociación:

irb> author.books.costs_more_than(100.10)

14.2 Usar Condicionales

Tu alcance puede utilizar condicionales:

class Order < ApplicationRecord
  scope :created_before, ->(time) { where(created_at: ...time) if time.present? }
end

Al igual que los otros ejemplos, esto se comportará de manera similar a un método de clase.

class Order < ApplicationRecord
  def self.created_before(time)
    where(created_at: ...time) if time.present?
  end
end

Sin embargo, hay una advertencia importante: Un alcance siempre devolverá un objeto ActiveRecord::Relation, incluso si el condicional evalúa a false, mientras que un método de clase, devolverá nil. Esto puede causar NoMethodError al encadenar métodos de clase con condicionales, si alguno de los condicionales devuelve false.

14.3 Aplicar un Alcance Predeterminado

Si deseamos que un alcance se aplique a todas las consultas al modelo, podemos usar el método [default_scope][] dentro del propio modelo.

class Book < ApplicationRecord
  default_scope { where(out_of_print: false) }
end

Cuando se ejecutan consultas en este modelo, la consulta SQL ahora se verá así:

SELECT * FROM books WHERE (out_of_print = false)

Si necesitas hacer cosas más complejas con un alcance predeterminado, puedes alternativamente definirlo como un método de clase:

class Book < ApplicationRecord
  def self

Comentarios

Se te anima a ayudar a mejorar la calidad de esta guía.

Por favor contribuye si ves algún error tipográfico o errores fácticos. Para comenzar, puedes leer nuestra sección de contribuciones a la documentación.

También puedes encontrar contenido incompleto o cosas que no están actualizadas. Por favor agrega cualquier documentación faltante para main. Asegúrate de revisar Guías Edge primero para verificar si los problemas ya están resueltos o no en la rama principal. Revisa las Guías de Ruby on Rails para estilo y convenciones.

Si por alguna razón detectas algo que corregir pero no puedes hacerlo tú mismo, por favor abre un issue.

Y por último, pero no menos importante, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.