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
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:
annotate
find
create_with
distinct
eager_load
extending
extract_associated
from
group
having
includes
joins
left_outer_joins
limit
lock
none
offset
optimizer_hints
order
preload
readonly
references
reorder
reselect
regroup
reverse_order
select
where
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 llamadaafter_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.