1 ¿Qué es la interfaz de consulta de Active Record?
Si estás acostumbrado a utilizar SQL en bruto para encontrar registros de la base de datos, generalmente encontrarás que hay mejores formas de realizar las mismas operaciones en Rails. Active Record te protege de la necesidad de utilizar 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, incluyendo MySQL, MariaDB, PostgreSQL y SQLite. Independientemente del sistema de bases 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 siguientes modelos utilizan 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 Recuperando 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 de la siguiente manera:
- 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 los callbacks
after_find
y luegoafter_initialize
, si los hay.
2.1 Recuperando un solo objeto
Active Record proporciona varias formas diferentes de recuperar un solo objeto.
2.1.1 find
Utilizando 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 la 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
lanzará una excepción ActiveRecord::RecordNotFound
si no se encuentra ningún registro coincidente.
También puedes utilizar este método para consultar varios objetos. Llama al método find
y pasa un array de claves primarias. El resultado será un array que contiene todos los registros coincidentes para las claves primarias suministradas. Por ejemplo:
```irb
Encuentra los clientes con claves primarias 1 y 10.
irb> customers = Customer.find([1, 10]) # O Customer.find(1, 10)
=> [#
El equivalente SQL de lo anterior es:
SELECT * FROM customers WHERE (customers.id IN (1,10))
ADVERTENCIA: El método find
lanzará una excepción ActiveRecord::RecordNotFound
a menos que se encuentre un registro coincidente para todos las claves primarias suministradas.
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 lanzará ninguna excepción.
Puede 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 lanzará ActiveRecord::RecordNotFound
si no se encuentra ningún 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 ningún registro coincidente y no se lanzará ninguna excepción.
Si su ámbito predeterminado contiene un método de orden, first
devolverá el primer registro según este orden.
Puede 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
En una colección que se ordena 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 lanzará ActiveRecord::RecordNotFound
si no se encuentra ningún 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 ningún registro coincidente y no se lanzará ninguna excepción.
Si su ámbito predeterminado contiene un método de orden, last
devolverá el último registro según este orden.
Puede 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 se ordena 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 lanzará ActiveRecord::RecordNotFound
si no se encuentra ningún 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 varios registros, debes aplicar un orden para garantizar un resultado determinista.
El método find_by!
se comporta exactamente como find_by
, excepto que generará una excepción ActiveRecord::RecordNotFound
si no se encuentra ningún registro coincidente. Por ejemplo:
irb> Customer.find_by! first_name: 'no existe'
ActiveRecord::RecordNotFound
Esto es equivalente a escribir:
Customer.where(first_name: 'no existe').take!
2.2 Recuperar 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 buscar toda la tabla en un solo paso, 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, es posible que la colección completa exceda la cantidad de memoria disponible.
Rails proporciona dos métodos que abordan este problema dividiendo los registros en lotes que son amigables para 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 de una sola vez. Si solo necesitas iterar sobre 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, obteniendo más lotes según sea necesario, hasta que todos los registros hayan sido procesados.
find_each
funciona en clases de modelos, 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 y cuando no tengan un orden, 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 un ArgumentError
,
de lo contrario, se ignora el orden 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 la cantidad de registros que se deben recuperar en cada lote, antes de pasarlos 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
De forma predeterminada, los registros se recuperan en orden ascendente de la clave primaria. La opción :start
te permite configurar el primer ID de la secuencia cuando el ID más bajo no es 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 cuando el ID más alto no es 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 varios trabajadores manejen la misma
cola de procesamiento. Cada trabajador podría manejar 10000 registros configurando las
opciones :start
y :finish
adecuadas 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
.
ruby
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
devuelve lotes al bloque como un array de modelos, en lugar de individualmente. El siguiente ejemplo devolverá al bloque suministrado un array de hasta 1000 clientes a la vez, con el último bloque que contiene los clientes restantes:
# Dar a 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 modelos, como se ve arriba, y también en relaciones:
# Dar a 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 y cuando no tengan un orden, 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, se puede especificar la recuperación de lotes de 2500 registros de la siguiente manera:
Customer.find_in_batches(batch_size: 2500) do |customers|
export.add_customers(customers)
end
:start
La opción start
permite especificar el ID de inicio desde donde se seleccionarán los registros. Como se mencionó antes, de forma predeterminada, los registros se obtienen en orden ascendente de la clave primaria. Por ejemplo, para recuperar clientes a partir del 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 que se van a recuperar. El siguiente código 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 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 como cadena pura
Si deseas agregar condiciones a tu búsqueda, simplemente puedes 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 ataques 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 las condiciones usando un array.
3.2 Condiciones como array
Ahora, ¿qué pasa si ese título puede variar, por ejemplo, como un argumento de algún lugar? La búsqueda tendría entonces la siguiente 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 se reemplazará con el valor en params[:title]
y el segundo se reemplazará con la representación SQL de false
, que depende del adaptador.
Este código es muy preferible:
Book.where("title = ?", params[:title])
a este código:
Book.where("title = #{params[:title]}")
debido a la seguridad de los argumentos. Colocar 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 puede 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 prácticamente cualquier cosa con ella. Nunca coloques 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 con marcadores de posición
Similar al estilo de reemplazo de (?)
de los parámetros, también puedes especificar claves en tu cadena de condiciones junto con un hash de claves/valores correspondientes:
Book.where("created_at >= :start_date AND created_at <= :end_date",
{ start_date: params[:start_date], end_date: params[:end_date] })
Esto hace que sea más legible si tienes un gran número de condiciones variables.
3.2.2 Condiciones que usan LIKE
Aunque los argumentos de las condiciones se escapan automáticamente para prevenir la inyección SQL, los comodines LIKE
de SQL (es decir, %
y _
) no se escapan. Esto puede causar un comportamiento inesperado si se utiliza un valor no sanitizado en un argumento. Por ejemplo:
```ruby
Book.order(:title).order(:created_at)
OR
Book.order("title").order("created_at") ```
This will generate SQL like this:
SELECT * FROM books ORDER BY title, created_at
You can also use the reverse_order
method to reverse the order of the query:
Book.order(:created_at).reverse_order
# OR
Book.order("created_at").reverse_order
This will generate SQL like this:
SELECT * FROM books ORDER BY created_at DESC
3.3 Limit and Offset
To limit the number of records returned from the database, you can use the limit
method. For example, to retrieve the first 5 books:
Book.limit(5)
This will generate SQL like this:
SELECT * FROM books LIMIT 5
To retrieve a specific range of records, you can use the offset
method. For example, to retrieve records starting from the 6th record:
Book.offset(5)
This will generate SQL like this:
SELECT * FROM books OFFSET 5
You can also chain limit
and offset
together to retrieve a specific range of records:
Book.limit(5).offset(10)
This will generate SQL like this:
SELECT * FROM books LIMIT 5 OFFSET 10
3.4 Reordering
To reorder the records returned from the database, you can use the reorder
method. This allows you to specify a new order for the records, overriding any previous ordering.
For example, to retrieve the books ordered by title and then reorder them by created_at:
Book.order(:title).reorder(:created_at)
This will generate SQL like this:
SELECT * FROM books ORDER BY created_at
3.5 Selecting Specific Fields
By default, Active Record will retrieve all columns from the table when querying for records. However, you can specify specific fields to retrieve using the select
method.
For example, to retrieve only the title and author fields of the books:
Book.select(:title, :author)
This will generate SQL like this:
SELECT title, author FROM books
You can also use SQL expressions in the select statement:
Book.select("title", "UPPER(author) AS uppercase_author")
This will generate SQL like this:
SELECT title, UPPER(author) AS uppercase_author FROM books
3.6 Grouping
To group records by one or more columns, you can use the group
method. This is useful when you want to perform aggregate functions on the grouped records.
For example, to retrieve the total number of books published by each author:
Book.group(:author).count
This will generate SQL like this:
SELECT author, COUNT(*) AS count FROM books GROUP BY author
You can also use the having
method to specify conditions on the grouped records:
Book.group(:author).having("COUNT(*) > 1")
This will generate SQL like this:
SELECT author FROM books GROUP BY author HAVING COUNT(*) > 1
3.7 Joins
Active Record allows you to perform joins between tables using the joins
method. This allows you to retrieve records from multiple tables based on a common column.
For example, to retrieve all books with their corresponding authors:
Book.joins(:author)
This will generate SQL like this:
SELECT books.* FROM books INNER JOIN authors ON authors.id = books.author_id
You can also specify additional conditions on the join:
Book.joins(:author).where("authors.name = ?", "John Doe")
This will generate SQL like this:
SELECT books.* FROM books INNER JOIN authors ON authors.id = books.author_id WHERE authors.name = 'John Doe'
3.8 Eager Loading
By default, Active Record will perform a separate database query for each association when retrieving records. This can lead to the N+1 query problem, where the number of queries grows linearly with the number of records retrieved.
To avoid this problem, you can use eager loading to retrieve all associated records in a single query. This can be done using the includes
method.
For example, to retrieve all books with their corresponding authors:
Book.includes(:author)
This will generate SQL like this:
SELECT books.* FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id
You can also specify multiple associations to eager load:
Book.includes(:author, :publisher)
This will generate SQL like this:
SELECT books.* FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id LEFT OUTER JOIN publishers ON publishers.id = books.publisher_id
Eager loading can also be used with nested associations:
Book.includes(author: :publisher)
This will generate SQL like this:
SELECT books.* FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id LEFT OUTER JOIN publishers ON publishers.id = authors.publisher_id
3.9 Locking
Active Record allows you to lock records in the database to prevent other processes from modifying them. This can be done using the lock
method.
For example, to lock a book record:
book = Book.find(1)
book.lock!
This will generate SQL like this:
SELECT * FROM books WHERE books.id = 1 FOR UPDATE
You can also use the lock
method with a block to lock multiple records:
Book.lock do
books = Book.where(out_of_print: true)
# Perform operations on locked records
end
This will generate SQL like this:
SELECT * FROM books WHERE books.out_of_print = 1 FOR UPDATE
3.10 Pluck
Active Record allows you to retrieve a single column from the database using the pluck
method. This can be useful when you only need a specific attribute of the records.
For example, to retrieve the titles of all books:
Book.pluck(:title)
This will generate SQL like this:
SELECT title FROM books
You can also retrieve multiple columns:
Book.pluck(:title, :author)
This will generate SQL like this:
SELECT title, author FROM books
3.11 Calculations
Active Record allows you to perform calculations on the records retrieved from the database using the [calculate
][] method. This can be useful when you need to retrieve aggregate values, such as the sum, average, or maximum of a column.
For example, to retrieve the total number of books:
Book.calculate(:count, :all)
This will generate SQL like this:
SELECT COUNT(*) FROM books
You can also perform calculations on a specific column:
Book.calculate(:sum, :price)
This will generate SQL like this:
SELECT SUM(price) FROM books
You can also specify conditions on the records to be included in the calculation:
Book.calculate(:count, :all, conditions: { out_of_print: true })
This will generate SQL like this:
SELECT COUNT(*) FROM books WHERE books.out_of_print = 1
3.12 Batches
Active Record allows you to retrieve records from the database in batches using the find_each
and find_in_batches
methods. This can be useful when you need to process a large number of records without loading them all into memory at once.
For example, to process all books in batches of 100:
Book.find_each(batch_size: 100) do |book|
# Process book
end
You can also use the find_in_batches
method to retrieve records in batches:
Book.find_in_batches(batch_size: 100) do |books|
# Process batch of books
end
Both methods will retrieve records in batches and yield them to the block for processing. This allows you to work with a subset of records at a time, reducing memory usage.
3.13 Transactions
Active Record allows you to perform multiple database operations within a single transaction using the [transaction
][] method. This ensures that all operations are committed or rolled back as a single unit.
For example, to create a new book and update an existing author within a transaction:
Book.transaction do
book = Book.create(title: "New Book")
author = Author.find(1)
author.update(name: "New Name")
end
If any of the operations within the transaction fail, the entire transaction will be rolled back and no changes will be made to the database.
3.14 Raw SQL Queries
Active Record allows you to execute raw SQL queries using the find_by_sql
method. This can be useful when you need to perform complex queries that cannot be expressed using the Active Record query interface.
For example, to retrieve all books with a title containing the word "Rails":
Book.find_by_sql("SELECT * FROM books WHERE title LIKE '%Rails%'")
This will execute the raw SQL query and return an array of Book objects.
You can also use the [exec_query
][] method to execute a raw SQL query and return a result object:
result = Book.connection.exec_query("SELECT * FROM books WHERE title LIKE '%Rails%'")
This will execute the raw SQL query and return a result object that can be used to access the query results.
3.15 Conclusion
Active Record provides a powerful and flexible interface for querying and manipulating database records. By understanding and utilizing the various query methods and options available, you can write efficient and expressive code for working with your database.
irb
irb> Book.order("title ASC").order("created_at DESC")
SELECT * FROM books ORDER BY title ASC, created_at DESC
ADVERTENCIA: En la mayoría de los sistemas de bases de datos, al seleccionar campos con distinct
de un conjunto de resultados utilizando métodos como select
, pluck
e ids
; el método order
generará una excepción ActiveRecord::StatementInvalid
a menos que el campo(s) utilizado(s) en la cláusula order
estén incluidos en la lista de selección. Consulte la siguiente sección para seleccionar campos del conjunto de resultados.
4 Selección de campos específicos
Por defecto, Model.find
selecciona todos los campos del conjunto de resultados utilizando select *
.
Para seleccionar solo un subconjunto de campos del conjunto de resultados, puede 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 así:
SELECT isbn, out_of_print FROM books
Tenga cuidado porque esto también significa que está inicializando un objeto de modelo con solo los campos que ha seleccionado. Si intenta acceder a un campo que no está en el registro inicializado, recibirá:
ActiveModel::MissingAttributeError: missing attribute '<attribute>' for Book
Donde <attribute>
es el atributo que solicitó. El método id
no generará la excepción ActiveRecord::MissingAttributeError
, así que tenga cuidado al trabajar con asociaciones porque necesitan el método id
para funcionar correctamente.
Si desea obtener solo un registro único por valor único en un campo determinado, puede usar distinct
:
Customer.select(:last_name).distinct
Esto generaría SQL como:
SELECT DISTINCT last_name FROM customers
También puede eliminar la restricción de unicidad:
# Devuelve last_names únicos
query = Customer.select(:last_name).distinct
# Devuelve todos los last_names, incluso si hay duplicados
query.distinct(false)
5 Límite y desplazamiento
Para aplicar LIMIT
a la consulta SQL generada por Model.find
, puede especificar el LIMIT
utilizando los métodos limit
y offset
en la relación.
Puede usar limit
para especificar el número de registros que se van a recuperar y usar offset
para especificar el número de registros que se deben 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 de la tabla. La consulta SQL que ejecuta se ve así:
SELECT * FROM customers LIMIT 5
Agregando offset
a eso
Customer.limit(5).offset(30)
en cambio, devolverá un máximo de 5 clientes a partir del 31º. El SQL se ve así:
SELECT * FROM customers LIMIT 5 OFFSET 30
6 Agrupamiento
Para aplicar una cláusula GROUP BY
a la consulta SQL generada por el buscador, puede usar el método group
.
Por ejemplo, si desea encontrar una colección de las fechas en las que se crearon los pedidos:
Order.select("created_at").group("created_at")
Y esto le dará un objeto Order
único para cada fecha en la que haya pedidos en la base de datos.
La consulta SQL que se ejecutaría sería algo así:
SELECT created_at
FROM orders
GROUP BY created_at
6.1 Total de elementos agrupados
Para obtener el total de elementos agrupados en una sola consulta, llame a count
después del group
.
irb> Order.group(:status).count
=> {"being_packed"=>7, "shipped"=>12}
La consulta SQL que se ejecutaría sería algo así:
SELECT COUNT (*) AS count_all, status AS status
FROM orders
GROUP BY status
6.2 Condiciones HAVING
SQL utiliza la cláusula HAVING
para especificar condiciones en los campos GROUP BY
. Puede agregar la cláusula HAVING
a la consulta SQL generada por Model.find
agregando el método having
a la consulta.
Por ejemplo:
Order.select("created_at, sum(total) as total_price").
group("created_at").having("sum(total) > ?", 200)
La consulta SQL que se ejecutaría sería algo así:
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 realizaron y donde el total es superior a $200.
Puede acceder al total_price
para cada objeto de pedido devuelto de esta manera:
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 pedido
7 Anulación de condiciones
7.1 unscope
Puede especificar ciertas condiciones que se eliminarán utilizando el método unscope
. Por ejemplo:
ruby
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 cláusulas where
específicas. Por ejemplo, esto eliminará la condición de 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 utilizado unscope
afectará a cualquier relación en la que se fusiona:
Book.order('id desc').merge(Book.unscope(:order))
# SELECT books.* FROM books
7.2 only
También puedes anular condiciones utilizando 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
7.3 reselect
El método reselect
anula una declaración de selección 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 en el que no se utiliza la cláusula reselect
:
Book.select(:title, :isbn).select(:created_at)
El SQL que se ejecutaría:
SELECT books.title, books.isbn, books.created_at FROM books
7.4 reorder
El método reorder
anula el orden predeterminado del ámbito. Por ejemplo, si la definición de la 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
7.5 reverse_order
El método reverse_order
invierte la cláusula de orden si se especifica.
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, 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.
7.6 rewhere
El método [rewhere
][] anula 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 utiliza la cláusula rewhere
, las cláusulas where
se combinan mediante el operador AND:
Book.where(out_of_print: true).where(out_of_print: false)
El SQL que se ejecutaría:
SELECT * FROM books WHERE out_of_print = 1 AND out_of_print = 0
7.7 regroup
El método regroup
anula 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 utiliza la cláusula regroup
, las cláusulas group
se combinan:
Book.group(:author).group(:id)
El SQL que se ejecutaría:
SELECT * FROM books GROUP BY author, id
8 Relación nula
El método none
devuelve una relación encadenable sin registros. Cualquier condición posterior encadenada a la relación devuelta seguirá generando relaciones vacías. Esto es útil en escenarios donde necesitas una respuesta encadenable para un método o un ámbito que podría devolver cero resultados.
Book.none # devuelve una Relación vacía y no realiza consultas.
# Se espera que el método highlighted_reviews a continuación siempre devuelva una Relación.
Book.first.highlighted_reviews.average(:rating)
# => Devuelve la calificación promedio de un libro
class Book
# Devuelve las reseñas si hay al menos 5,
# de lo contrario, considera este libro como no reseñado
def highlighted_reviews
if reviews.count > 5
reviews
else
Review.none # No cumple aún el umbral mínimo
end
end
end
9 Objetos de solo lectura
Active Record proporciona el método readonly
en una relación para prohibir explícitamente la modificación de cualquiera de los objetos devueltos. Cualquier intento de modificar un registro de solo lectura no tendrá éxito y generará una excepción ActiveRecord::ReadOnlyRecord
.
ruby
customer = Customer.readonly.first
customer.visits += 1
customer.save
Como customer
está explícitamente configurado 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.
10 Bloqueo de registros para actualización
El bloqueo es útil para prevenir condiciones de carrera al actualizar registros en la base de datos y garantizar actualizaciones atómicas.
Active Record proporciona dos mecanismos de bloqueo:
- Bloqueo optimista
- Bloqueo pesimista
10.1 Bloqueo optimista
El bloqueo optimista permite que varios usuarios accedan al mismo registro para realizar ediciones y asume un mínimo de conflictos con los datos. Esto se logra 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 se ignora la actualización.
Columna de bloqueo optimista
Para utilizar el bloqueo optimista, la tabla debe 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 menor en el campo lock_version
que el que se encuentra 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
Entonces, es responsabilidad del programador manejar el conflicto rescatando la excepción y ya sea deshaciendo los cambios, fusionándolos o aplicando la lógica de negocio necesaria para resolver el conflicto.
Este comportamiento se puede desactivar configurando ActiveRecord::Base.lock_optimistically = false
.
Para cambiar 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
10.2 Bloqueo pesimista
El bloqueo pesimista utiliza un mecanismo de bloqueo proporcionado por la base de datos subyacente. Al utilizar lock
al construir una relación, se obtiene un bloqueo exclusivo en las filas seleccionadas. Las relaciones que utilizan lock
generalmente se envuelven dentro de una transacción para evitar condiciones de bloqueo.
Por ejemplo:
Book.transaction do
book = Book.lock.first
book.title = 'Algorithms, second edition'
book.save!
end
La sesión anterior produce la siguiente consulta SQL para una base de datos 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 sin procesar 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 permitir que otras consultas lo lean. Para especificar esta expresión, simplemente pásala como 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 admitir el SQL sin procesar que pasas 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 utilizando el siguiente código:
book = Book.first
book.with_lock do
# Este bloque se ejecuta dentro de una transacción,
# el libro ya está bloqueado.
book.increment!(:views)
end
11 Unir 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 utiliza para INNER JOIN
o consultas personalizadas,
left_outer_joins
se utiliza para consultas que utilizan LEFT OUTER JOIN
.
11.1 joins
Hay varias formas de utilizar el método joins
.
11.1.1 Usando un fragmento de SQL en cadena
Puedes simplemente proporcionar el SQL sin procesar que especifica la cláusula JOIN
a joins
:
Author.joins("INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE")
Esto dará como resultado el siguiente SQL:
SELECT authors.* FROM authors INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE
11.1.2 Usando un array/Hash de asociaciones con nombres
Active Record te permite utilizar los nombres de las asociaciones definidas en el modelo como un atajo para especificar cláusulas JOIN
para esas asociaciones al utilizar el método joins
.
Todas las siguientes producirán las consultas de unión esperadas utilizando INNER JOIN
:
11.1.2.1 Unir una sola asociación
Book.joins(:reviews)
Esto produce:
SELECT books.* FROM books
INNER JOIN reviews ON reviews.book_id = books.id
O, en español: "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 quieres libros únicos, puedes usar Book.joins(:reviews).distinct
.
11.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 español: "devuelve todos los libros con su autor que tienen al menos una reseña". Nuevamente, tenga en cuenta que los libros con múltiples reseñas aparecerán varias veces.
11.1.3.1 Uniendo asociaciones anidadas (un solo nivel)
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 español: "devuelve todos los libros que tienen una reseña de un cliente".
11.1.3.2 Uniendo asociaciones anidadas (varios niveles)
Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])
Esto produce:
SELECT * 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 español: "devuelve todos los autores que tienen libros con reseñas y han sido ordenados por un cliente, y los proveedores de esos libros".
11.1.4 Especificando condiciones en las tablas unidas
Puede especificar condiciones en las tablas unidas utilizando las condiciones regulares de Array y String. 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 se crearon ayer, utilizando una expresión SQL BETWEEN
para comparar created_at
.
Una sintaxis alternativa y más limpia es anidar las condiciones de 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 ámbito con nombre existente, se puede utilizar merge
. Primero, agreguemos un nuevo ámbito con nombre 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 ámbito 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 se crearon ayer, nuevamente utilizando una expresión SQL BETWEEN
.
11.2 left_outer_joins
Si desea seleccionar un conjunto de registros, independientemente de si tienen registros asociados o no, puede utilizar el método left_outer_joins
.
Customer.left_outer_joins(:reviews).distinct.select('customers.*, COUNT(reviews.*) AS reviews_count').group('customers.id')
Lo cual 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 cuenta de reseñas, independientemente de si tienen o no reseñas".
11.3 where.associated
y where.missing
Los métodos de consulta associated
y missing
le 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 realizado 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 realizado ninguna reseña".
12 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 posible de consultas.
12.1 Problema de las consultas N + 1
Considere el siguiente código, que encuentra 10 libros e imprime los apellidos de sus autores:
books = Book.limit(10)
books.each do |book|
puts book.author.last_name
end
Este código parece estar bien 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.
12.1.1 Solución al problema de las consultas N + 1
Active Record le permite especificar de antemano todas las asociaciones que se van a cargar.
Los métodos son:
12.2 includes
Con includes
, Active Record asegura que todas las asociaciones especificadas se carguen utilizando la menor cantidad posible de consultas.
Revisitando el caso anterior utilizando el método includes
, podríamos reescribir Book.limit(10)
para cargar de forma anticipada los 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.book_id IN (1,2,3,4,5,6,7,8,9,10)
12.2.1 Carga ansiosa de múltiples asociaciones
Active Record te permite cargar ansiosamente cualquier número de asociaciones con una sola llamada a Model.find
utilizando un array, hash o un hash anidado de array/hash con el método includes
.
12.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.
12.2.1.2 Hash de asociaciones anidadas
Customer.includes(orders: { books: [:supplier, :author] }).find(1)
Esto encontrará al cliente con id 1 y cargará ansiosamente todos los pedidos asociados, los libros de todos los pedidos y el autor y proveedor de cada libro.
12.2.2 Especificar condiciones en asociaciones cargadas ansiosamente
Aunque Active Record te permite especificar condiciones en las asociaciones cargadas ansiosamente al igual que en joins
, la forma recomendada es usar joins en su lugar.
Sin embargo, si es necesario 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 uno utilizando 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 una 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 SQL, necesitas usar references
para forzar las 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 ansiosamente como parte de una unión, cualquier campo de una cláusula de selección personalizada no estará presente en los modelos cargados. Esto se debe a que es ambiguo si deben aparecer en el registro padre o en el hijo.
12.3 preload
Con preload
, Active Record carga cada asociación especificada utilizando una consulta por asociación.
Revisando el problema de las consultas N + 1, podríamos reescribir Book.limit(10)
para precargar los 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.book_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 las asociaciones precargadas.
12.4 eager_load
Con eager_load
, Active Record carga todas las asociaciones especificadas utilizando un LEFT OUTER JOIN
.
Revisando el caso donde ocurrió N + 1 utilizando el método eager_load
, podríamos reescribir Book.limit(10)
para los autores:
books = Book.eager_load(: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 DISTINCT books.id FROM books LEFT OUTER JOIN authors ON authors.book_id = books.id LIMIT 10
SELECT books.id AS t0_r0, books.last_name AS t0_r1, ...
FROM books LEFT OUTER JOIN authors ON authors.book_id = books.id
WHERE books.id IN (1,2,3,4,5,6,7,8,9,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 las asociaciones cargadas ansiosamente.
12.5 strict_loading
La carga ansiosa puede evitar las consultas N + 1, pero aún puedes estar cargando perezosamente algunas asociaciones. Para asegurarte de que no se carguen perezosamente ninguna asociación, 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 perezosamente una asociación:
user = User.strict_loading.first
user.comments.to_a # genera un ActiveRecord::StrictLoadingViolationError
13 Ámbitos
El ámbito permite especificar consultas comúnmente utilizadas que se pueden referenciar como llamadas de método en los objetos o modelos de asociación. Con estos ámbitos, se pueden utilizar todos los métodos previamente cubiertos, como where
, joins
e includes
. Todos los cuerpos de ámbito deben devolver un objeto ActiveRecord::Relation
o nil
para permitir que se llamen a otros métodos (como otros ámbitos) sobre él.
Para definir un ámbito simple, utilizamos el método scope
dentro de la clase, pasando la consulta que nos gustaría ejecutar cuando se llame a este ámbito:
class Book < ApplicationRecord
scope :out_of_print, -> { where(out_of_print: true) }
end
Para llamar a este ámbito out_of_print
, podemos llamarlo en la clase:
irb> Book.out_of_print
=> #<ActiveRecord::Relation> # todos los libros fuera de impresión
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 fuera de impresión de `author`
Los ámbitos también se pueden encadenar dentro de otros ámbitos:
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
13.1 Pasando argumentos
Su ámbito puede tomar argumentos:
class Book < ApplicationRecord
scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end
Llame al ámbito como si fuera un método de clase:
irb> Book.costs_more_than(100.10)
Sin embargo, esto solo duplica la funcionalidad que se le proporcionaría mediante 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)
13.2 Uso de condicionales
Su ámbito 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 ámbito siempre devolverá un objeto ActiveRecord::Relation
, incluso si la condición se evalúa como false
, mientras que un método de clase devolverá nil
. Esto puede causar un NoMethodError
al encadenar métodos de clase con condicionales, si alguno de los condicionales devuelve false
.
13.3 Aplicación de un ámbito predeterminado
Si deseamos que un ámbito se aplique en 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 se verá así:
SELECT * FROM books WHERE (out_of_print = false)
Si necesita hacer cosas más complejas con un ámbito predeterminado, también puede definirlo como un método de clase:
class Book < ApplicationRecord
def self.default_scope
# Debe devolver un objeto ActiveRecord::Relation.
end
end
NOTA: El default_scope
también se aplica al crear/construir un registro cuando se proporcionan los argumentos del ámbito como un Hash
. No se aplica al actualizar un registro. Ej.:
class Book < ApplicationRecord
default_scope { where(out_of_print: false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: false>
irb> Book.unscoped.new
=> #<Book id: nil, out_of_print: nil>
Tenga en cuenta que, cuando se proporciona en formato Array
, los argumentos de consulta de default_scope
no se pueden convertir en un Hash
para la asignación de atributos predeterminados. Ej.:
class Book < ApplicationRecord
default_scope { where("out_of_print = ?", false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: nil>
13.4 Fusión de ámbitos
Al igual que las cláusulas where
, los ámbitos se fusionan utilizando condiciones AND
.
class Book < ApplicationRecord
scope :in_print, -> { where(out_of_print: false) }
scope :out_of_print, -> { where(out_of_print: true) }
scope :recent, -> { where(year_published: 50.years.ago.year..) }
scope :old, -> { where(year_published: ...50.years.ago.year) }
end
irb> Book.out_of_print.old
SELECT books.* FROM books WHERE books.out_of_print = 'true' AND books.year_published < 1969
Podemos mezclar y combinar condiciones de scope
y where
y la consulta SQL final tendrá todas las condiciones unidas con AND
.
irb> Book.in_print.where(price: ...100)
SELECT books.* FROM books WHERE books.out_of_print = 'false' AND books.price < 100
Si queremos que la última cláusula where
sea la que prevalezca, se puede usar merge
.
irb> Book.in_print.merge(Book.out_of_print)
SELECT books.* FROM books WHERE books.out_of_print = true
Una advertencia importante es que el default_scope
se colocará al principio de las condiciones de scope
y where
.
```ruby
class Book < ApplicationRecord
default_scope { where(year_published: 50.years.ago.year..) }
scope :in_print, -> { where(out_of_print: false) } scope :out_of_print, -> { where(out_of_print: true) } end ```
irb> Book.all
SELECT books.* FROM books WHERE (year_published >= 1969)
irb> Book.in_print
SELECT books.* FROM books WHERE (year_published >= 1969) AND books.out_of_print = false
irb> Book.where('price > 50')
SELECT books.* FROM books WHERE (year_published >= 1969) AND (price > 50)
Como se puede ver arriba, el default_scope
se está fusionando en las condiciones de ambos scope
y where
.
13.5 Eliminando todos los ámbitos
Si deseamos eliminar los ámbitos por cualquier motivo, podemos usar el método unscoped
. Esto es especialmente útil si se especifica un default_scope
en el modelo y no se debe aplicar para esta consulta en particular.
Book.unscoped.load
Este método elimina todos los ámbitos y realizará una consulta normal en la tabla.
irb> Book.unscoped.all
SELECT books.* FROM books
irb> Book.where(out_of_print: true).unscoped.all
SELECT books.* FROM books
unscoped
también puede aceptar un bloque:
irb> Book.unscoped { Book.out_of_print }
SELECT books.* FROM books WHERE books.out_of_print
14 Buscadores dinámicos
Para cada campo (también conocido como atributo) que definas en tu tabla, Active Record proporciona un método buscador. Si tienes un campo llamado first_name
en tu modelo Customer
, por ejemplo, obtienes el método de instancia find_by_first_name
de forma gratuita desde Active Record. Si también tienes un campo locked
en el modelo Customer
, también obtienes el método find_by_locked
.
Puedes especificar un signo de exclamación (!
) al final de los buscadores dinámicos para que generen un error ActiveRecord::RecordNotFound
si no devuelven ningún registro, como Customer.find_by_first_name!("Ryan")
Si quieres buscar tanto por first_name
como por orders_count
, puedes encadenar estos buscadores simplemente escribiendo "and
" entre los campos. Por ejemplo, Customer.find_by_first_name_and_orders_count("Ryan", 5)
.
15 Enums
Un enum te permite definir una matriz de valores para un atributo y referirte a ellos por nombre. El valor real almacenado en la base de datos es un entero que se ha asignado a uno de los valores.
Declarar un enum:
- Crea ámbitos que se pueden usar para encontrar todos los objetos que tienen o no tienen uno de los valores del enum.
- Crea un método de instancia que se puede usar para determinar si un objeto tiene un valor particular para el enum.
- Crea un método de instancia que se puede usar para cambiar el valor del enum de un objeto.
para todos los posibles valores de un enum.
Por ejemplo, dada esta declaración de enum
:
class Order < ApplicationRecord
enum :status, [:shipped, :being_packaged, :complete, :cancelled]
end
Estos ámbitos se crean automáticamente y se pueden usar para encontrar todos los objetos con o sin un valor particular para status
:
irb> Order.shipped
=> #<ActiveRecord::Relation> # todos los pedidos con status == :shipped
irb> Order.not_shipped
=> #<ActiveRecord::Relation> # todos los pedidos con status != :shipped
Estos métodos de instancia se crean automáticamente y consultan si el modelo tiene ese valor para el enum status
:
irb> order = Order.shipped.first
irb> order.shipped?
=> true
irb> order.complete?
=> false
Estos métodos de instancia se crean automáticamente y primero actualizan el valor de status
al valor nombrado y luego consultan si el estado se ha establecido correctamente en el valor:
irb> order = Order.first
irb> order.shipped!
UPDATE "orders" SET "status" = ?, "updated_at" = ? WHERE "orders"."id" = ? [["status", 0], ["updated_at", "2019-01-24 07:13:08.524320"], ["id", 1]]
=> true
La documentación completa sobre enums se puede encontrar aquí.
16 Entendiendo el encadenamiento de métodos
El patrón Active Record implementa el encadenamiento de métodos, que nos permite usar múltiples métodos de Active Record juntos de manera simple y directa.
Puedes encadenar métodos en una declaración cuando el método anterior llamado devuelve un
ActiveRecord::Relation
, como all
, where
y joins
. Los métodos que devuelven
un solo objeto (ver la sección Recuperar un solo objeto)
deben estar al final de la declaración.
A continuación, se muestran algunos ejemplos. Esta guía no cubrirá todas las posibilidades, solo algunas como ejemplos. Cuando se llama a un método de Active Record, la consulta no se genera ni se envía a la base de datos de inmediato. La consulta se envía solo cuando los datos realmente se necesitan. Por lo tanto, cada ejemplo a continuación genera una sola consulta.
16.1 Recuperar datos filtrados de múltiples tablas
Customer
.select('customers.id, customers.last_name, reviews.body')
.joins(:reviews)
.where('reviews.created_at > ?', 1.week.ago)
El resultado debería ser algo como esto:
SELECT customers.id, customers.last_name, reviews.body
FROM customers
INNER JOIN reviews
ON reviews.customer_id = customers.id
WHERE (reviews.created_at > '2019-01-08')
16.2 Recuperando datos específicos de múltiples tablas
Book
.select('books.id, books.title, authors.first_name')
.joins(:author)
.find_by(title: 'Abstraction and Specification in Program Development')
Lo anterior debería generar:
SELECT books.id, books.title, authors.first_name
FROM books
INNER JOIN authors
ON authors.id = books.author_id
WHERE books.title = $1 [["title", "Abstraction and Specification in Program Development"]]
LIMIT 1
NOTA: Tenga en cuenta que si una consulta coincide con varios registros, find_by
solo obtendrá el primero e ignorará los demás (ver la declaración LIMIT 1
anterior).
17 Encontrar o construir un nuevo objeto
Es común que necesite encontrar un registro o crearlo si no existe. Puede hacerlo con los métodos find_or_create_by
y find_or_create_by!
.
17.1 find_or_create_by
El método find_or_create_by
verifica si existe un registro con los atributos especificados. Si no existe, se llama a create
. Veamos un ejemplo.
Supongamos que desea encontrar un cliente llamado "Andy" y, si no existe, crear uno. Puede hacerlo ejecutando:
irb> Customer.find_or_create_by(first_name: 'Andy')
=> #<Customer id: 5, first_name: "Andy", last_name: nil, title: nil, visits: 0, orders_count: nil, lock_version: 0, created_at: "2019-01-17 07:06:45", updated_at: "2019-01-17 07:06:45">
El SQL generado por este método se ve así:
SELECT * FROM customers WHERE (customers.first_name = 'Andy') LIMIT 1
BEGIN
INSERT INTO customers (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT
find_or_create_by
devuelve el registro que ya existe o el nuevo registro. En nuestro caso, no teníamos un cliente llamado Andy, por lo que se crea y devuelve el registro.
Es posible que el nuevo registro no se guarde en la base de datos; eso depende de si las validaciones se aprobaron o no (como create
).
Supongamos que queremos establecer el atributo 'locked' en false
si estamos creando un nuevo registro, pero no queremos incluirlo en la consulta. Entonces queremos encontrar al cliente llamado "Andy" o, si ese cliente no existe, crear un cliente llamado "Andy" que no esté bloqueado.
Podemos lograr esto de dos maneras. La primera es usar create_with
:
Customer.create_with(locked: false).find_or_create_by(first_name: 'Andy')
La segunda forma es usar un bloque:
Customer.find_or_create_by(first_name: 'Andy') do |c|
c.locked = false
end
El bloque solo se ejecutará si se está creando el cliente. La segunda vez que ejecutemos este código, se ignorará el bloque.
17.2 find_or_create_by!
También puede usar find_or_create_by!
para generar una excepción si el nuevo registro no es válido. Las validaciones no se cubren en esta guía, pero supongamos por un momento que agrega temporalmente
validates :orders_count, presence: true
a su modelo Customer
. Si intenta crear un nuevo Customer
sin pasar un orders_count
, el registro será inválido y se generará una excepción:
irb> Customer.find_or_create_by!(first_name: 'Andy')
ActiveRecord::RecordInvalid: Validation failed: Orders count can’t be blank
17.3 find_or_initialize_by
El método find_or_initialize_by
funcionará de manera similar a find_or_create_by
, pero llamará a new
en lugar de create
. Esto significa que se creará una nueva instancia del modelo en la memoria pero no se guardará en la base de datos. Continuando con el ejemplo de find_or_create_by
, ahora queremos al cliente llamado 'Nina':
irb> nina = Customer.find_or_initialize_by(first_name: 'Nina')
=> #<Customer id: nil, first_name: "Nina", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
irb> nina.persisted?
=> false
irb> nina.new_record?
=> true
Como el objeto aún no se almacena en la base de datos, el SQL generado se ve así:
SELECT * FROM customers WHERE (customers.first_name = 'Nina') LIMIT 1
Cuando desee guardarlo en la base de datos, simplemente llame a save
:
irb> nina.save
=> true
18 Búsqueda por SQL
Si desea utilizar su propio SQL para encontrar registros en una tabla, puede usar find_by_sql
. El método find_by_sql
devolverá una matriz de objetos incluso si la consulta subyacente devuelve solo un registro. Por ejemplo, podría ejecutar esta consulta:
irb> Customer.find_by_sql("SELECT * FROM customers INNER JOIN orders ON customers.id = orders.customer_id ORDER BY customers.created_at desc")
=> [#<Customer id: 1, first_name: "Lucas" ...>, #<Customer id: 2, first_name: "Jan" ...>, ...]
find_by_sql
te proporciona una forma sencilla de realizar llamadas personalizadas a la base de datos y recuperar objetos instanciados.
18.1 select_all
find_by_sql
tiene un método relacionado llamado connection.select_all
. select_all
recuperará
objetos de la base de datos utilizando SQL personalizado al igual que find_by_sql
, pero no los instanciará.
Este método devolverá una instancia de la clase ActiveRecord::Result
y llamar a to_a
en este
objeto te devolverá un array de hashes donde cada hash indica un registro.
irb> Customer.connection.select_all("SELECT first_name, created_at FROM customers WHERE id = '1'").to_a
=> [{"first_name"=>"Rafael", "created_at"=>"2012-11-10 23:23:45.281189"}, {"first_name"=>"Eileen", "created_at"=>"2013-12-09 11:22:35.221282"}]
18.2 pluck
pluck
se puede utilizar para seleccionar el valor o valores de la(s) columna(s) con nombre en la relación actual. Acepta una lista de nombres de columna como argumento y devuelve un array de valores de las columnas especificadas con el tipo de datos correspondiente.
irb> Book.where(out_of_print: true).pluck(:id)
SELECT id FROM books WHERE out_of_print = true
=> [1, 2, 3]
irb> Order.distinct.pluck(:status)
SELECT DISTINCT status FROM orders
=> ["shipped", "being_packed", "cancelled"]
irb> Customer.pluck(:id, :first_name)
SELECT customers.id, customers.first_name FROM customers
=> [[1, "David"], [2, "Fran"], [3, "Jose"]]
pluck
permite reemplazar código como:
Customer.select(:id).map { |c| c.id }
# o
Customer.select(:id).map(&:id)
# o
Customer.select(:id, :first_name).map { |c| [c.id, c.first_name] }
con:
Customer.pluck(:id)
# o
Customer.pluck(:id, :first_name)
A diferencia de select
, pluck
convierte directamente un resultado de la base de datos en un Array
de Ruby,
sin construir objetos ActiveRecord
. Esto puede significar un mejor rendimiento para
una consulta grande o que se ejecuta con frecuencia. Sin embargo, cualquier anulación de método del modelo
no estará disponible. Por ejemplo:
class Customer < ApplicationRecord
def name
"Soy #{first_name}"
end
end
irb> Customer.select(:first_name).map &:name
=> ["Soy David", "Soy Jeremy", "Soy Jose"]
irb> Customer.pluck(:first_name)
=> ["David", "Jeremy", "Jose"]
No estás limitado a consultar campos de una sola tabla, también puedes consultar varias tablas.
irb> Order.joins(:customer, :books).pluck("orders.created_at, customers.email, books.title")
Además, a diferencia de select
y otros ámbitos de Relation
, pluck
desencadena una consulta inmediata,
y por lo tanto no se puede encadenar con más ámbitos, aunque puede funcionar con
ámbitos ya construidos anteriormente:
irb> Customer.pluck(:first_name).limit(1)
NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
irb> Customer.limit(1).pluck(:first_name)
=> ["David"]
NOTA: También debes saber que usar pluck
desencadenará la carga ansiosa si el objeto de relación contiene valores de inclusión, incluso si la carga ansiosa no es necesaria para la consulta. Por ejemplo:
irb> assoc = Customer.includes(:reviews)
irb> assoc.pluck(:id)
SELECT "customers"."id" FROM "customers" LEFT OUTER JOIN "reviews" ON "reviews"."id" = "customers"."review_id"
Una forma de evitar esto es unscope
los includes:
irb> assoc.unscope(:includes).pluck(:id)
18.3 pick
pick
se puede utilizar para seleccionar el valor o valores de la(s) columna(s) con nombre en la relación actual. Acepta una lista de nombres de columna como argumento y devuelve la primera fila de los valores de columna especificados con el tipo de datos correspondiente.
pick
es una forma abreviada de relation.limit(1).pluck(*column_names).first
, que es especialmente útil cuando ya tienes una relación limitada a una fila.
pick
permite reemplazar código como:
Customer.where(id: 1).pluck(:id).first
con:
Customer.where(id: 1).pick(:id)
18.4 ids
ids
se puede utilizar para seleccionar todos los IDs de la relación utilizando la clave primaria de la tabla.
irb> Customer.ids
SELECT id FROM customers
class Customer < ApplicationRecord
self.primary_key = "customer_id"
end
irb> Customer.ids
SELECT customer_id FROM customers
19 Existencia de objetos
Si simplemente quieres verificar la existencia del objeto, hay un método llamado exists?
.
Este método consultará la base de datos utilizando la misma consulta que find
, pero en lugar de devolver un
objeto o una colección de objetos, devolverá true
o false
.
Customer.exists?(1)
El método exists?
también acepta múltiples valores, pero la trampa es que devolverá true
si alguno
de esos registros existe.
Customer.exists?(id: [1, 2, 3])
# o
Customer.exists?(first_name: ['Jane', 'Sergei'])
Incluso es posible usar exists?
sin argumentos en un modelo o una relación.
Customer.where(first_name: 'Ryan').exists?
Lo anterior devuelve true
si hay al menos un cliente con el first_name
'Ryan' y false
en caso contrario.
Customer.exists?
Lo anterior devuelve false
si la tabla customers
está vacía y true
en caso contrario.
También puedes usar any?
y many?
para verificar la existencia en un modelo o relación. many?
utilizará count
de SQL para determinar si el elemento existe.
```ruby
a través de un modelo
Order.any?
SELECT 1 FROM orders LIMIT 1
Order.many?
SELECT COUNT(*) FROM (SELECT 1 FROM orders LIMIT 2)
a través de un ámbito nombrado
Order.shipped.any?
SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 1
Order.shipped.many?
SELECT COUNT(*) FROM (SELECT 1 FROM orders WHERE orders.status = 0 LIMIT 2)
a través de una relación
Book.where(out_of_print: true).any? Book.where(out_of_print: true).many?
a través de una asociación
Customer.first.orders.any? Customer.first.orders.many? ```
20 Cálculos
Esta sección utiliza count
como método de ejemplo en este preámbulo, pero las opciones descritas se aplican a todas las subsecciones.
Todos los métodos de cálculo funcionan directamente en un modelo:
irb> Customer.count
SELECT COUNT(*) FROM customers
O en una relación:
irb> Customer.where(first_name: 'Ryan').count
SELECT COUNT(*) FROM customers WHERE (first_name = 'Ryan')
También puedes usar varios métodos de búsqueda en una relación para realizar cálculos complejos:
irb> Customer.includes("orders").where(first_name: 'Ryan', orders: { status: 'shipped' }).count
Lo cual ejecutará:
SELECT COUNT(DISTINCT customers.id) FROM customers
LEFT OUTER JOIN orders ON orders.customer_id = customers.id
WHERE (customers.first_name = 'Ryan' AND orders.status = 0)
asumiendo que Order tiene enum status: [ :shipped, :being_packed, :cancelled ]
.
20.1 count
Si quieres ver cuántos registros hay en la tabla de tu modelo, puedes llamar a Customer.count
y eso devolverá el número.
Si quieres ser más específico y encontrar todos los clientes con un título presente en la base de datos, puedes usar Customer.count(:title)
.
Para opciones, por favor vea la sección principal, Cálculos.
20.2 average
Si quieres ver el promedio de un cierto número en una de tus tablas, puedes llamar al método average
en la clase que se relaciona con la tabla. Esta llamada al método se verá algo así:
Order.average("subtotal")
Esto devolverá un número (posiblemente un número de punto flotante como 3.14159265) que representa el valor promedio en el campo.
Para opciones, por favor vea la sección principal, Cálculos.
20.3 minimum
Si quieres encontrar el valor mínimo de un campo en tu tabla, puedes llamar al método minimum
en la clase que se relaciona con la tabla. Esta llamada al método se verá algo así:
Order.minimum("subtotal")
Para opciones, por favor vea la sección principal, Cálculos.
20.4 maximum
Si quieres encontrar el valor máximo de un campo en tu tabla, puedes llamar al método maximum
en la clase que se relaciona con la tabla. Esta llamada al método se verá algo así:
Order.maximum("subtotal")
Para opciones, por favor vea la sección principal, Cálculos.
20.5 sum
Si quieres encontrar la suma de un campo para todos los registros en tu tabla, puedes llamar al método sum
en la clase que se relaciona con la tabla. Esta llamada al método se verá algo así:
Order.sum("subtotal")
Para opciones, por favor vea la sección principal, Cálculos.
21 Ejecutando EXPLAIN
Puedes ejecutar explain
en una relación. La salida de EXPLAIN varía para cada base de datos.
Por ejemplo, ejecutar
Customer.where(id: 1).joins(:orders).explain
puede dar como resultado
EXPLAIN SELECT `customers`.* FROM `customers` INNER JOIN `orders` ON `orders`.`customer_id` = `customers`.`id` WHERE `customers`.`id` = 1
+----+-------------+------------+-------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+------------+-------+---------------+
| 1 | SIMPLE | customers | const | PRIMARY |
| 1 | SIMPLE | orders | ALL | NULL |
+----+-------------+------------+-------+---------------+
+---------+---------+-------+------+-------------+
| key | key_len | ref | rows | Extra |
+---------+---------+-------+------+-------------+
| PRIMARY | 4 | const | 1 | |
| NULL | NULL | NULL | 1 | Using where |
+---------+---------+-------+------+-------------+
2 rows in set (0.00 sec)
en MySQL y MariaDB.
Active Record realiza una impresión en formato legible que emula la del shell de la base de datos correspondiente. Por lo tanto, la misma consulta ejecutada con el adaptador de PostgreSQL mostraría en su lugar
EXPLAIN SELECT "customers".* FROM "customers" INNER JOIN "orders" ON "orders"."customer_id" = "customers"."id" WHERE "customers"."id" = $1 [["id", 1]]
QUERY PLAN
------------------------------------------------------------------------------
Nested Loop (cost=4.33..20.85 rows=4 width=164)
-> Index Scan using customers_pkey on customers (cost=0.15..8.17 rows=1 width=164)
Index Cond: (id = '1'::bigint)
-> Bitmap Heap Scan on orders (cost=4.18..12.64 rows=4 width=8)
Recheck Cond: (customer_id = '1'::bigint)
-> Bitmap Index Scan on index_orders_on_customer_id (cost=0.00..4.18 rows=4 width=0)
Index Cond: (customer_id = '1'::bigint)
(7 rows)
La carga temprana puede desencadenar más de una consulta en el fondo, y algunas consultas
pueden necesitar los resultados de consultas anteriores. Debido a eso, explain
realmente
ejecuta la consulta y luego solicita los planes de consulta. Por ejemplo,
ruby
Customer.where(id: 1).includes(:orders).explain
puede generar esto para MySQL y MariaDB:
EXPLAIN SELECT `customers`.* FROM `customers` WHERE `customers`.`id` = 1
+----+-------------+-----------+-------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+-----------+-------+---------------+
| 1 | SIMPLE | customers | const | PRIMARY |
+----+-------------+-----------+-------+---------------+
+---------+---------+-------+------+-------+
| key | key_len | ref | rows | Extra |
+---------+---------+-------+------+-------+
| PRIMARY | 4 | const | 1 | |
+---------+---------+-------+------+-------+
1 fila en el conjunto (0.00 seg)
EXPLAIN SELECT `orders`.* FROM `orders` WHERE `orders`.`customer_id` IN (1)
+----+-------------+--------+------+---------------+
| id | select_type | table | type | possible_keys |
+----+-------------+--------+------+---------------+
| 1 | SIMPLE | orders | ALL | NULL |
+----+-------------+--------+------+---------------+
+------+---------+------+------+-------------+
| key | key_len | ref | rows | Extra |
+------+---------+------+------+-------------+
| NULL | NULL | NULL | 1 | Using where |
+------+---------+------+------+-------------+
1 fila en el conjunto (0.00 seg)
y puede generar esto para PostgreSQL:
Customer Load (0.3ms) SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 [["id", 1]]
Order Load (0.3ms) SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = $1 [["customer_id", 1]]
=> EXPLAIN SELECT "customers".* FROM "customers" WHERE "customers"."id" = $1 [["id", 1]]
QUERY PLAN
----------------------------------------------------------------------------------
Index Scan using customers_pkey on customers (cost=0.15..8.17 rows=1 width=164)
Index Cond: (id = '1'::bigint)
(2 filas)
21.1 Opciones de Explicación
Para bases de datos y adaptadores que las admiten (actualmente PostgreSQL y MySQL), se pueden pasar opciones para proporcionar un análisis más profundo.
Usando PostgreSQL, lo siguiente:
Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)
produce:
EXPLAIN (ANALYZE, VERBOSE) SELECT "shop_accounts".* FROM "shop_accounts" INNER JOIN "customers" ON "customers"."id" = "shop_accounts"."customer_id" WHERE "shop_accounts"."id" = $1 [["id", 1]]
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop (cost=0.30..16.37 rows=1 width=24) (actual time=0.003..0.004 rows=0 loops=1)
Output: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
Inner Unique: true
-> Index Scan using shop_accounts_pkey on public.shop_accounts (cost=0.15..8.17 rows=1 width=24) (actual time=0.003..0.003 rows=0 loops=1)
Output: shop_accounts.id, shop_accounts.customer_id, shop_accounts.customer_carrier_id
Index Cond: (shop_accounts.id = '1'::bigint)
-> Index Only Scan using customers_pkey on public.customers (cost=0.15..8.17 rows=1 width=8) (never executed)
Output: customers.id
Index Cond: (customers.id = shop_accounts.customer_id)
Heap Fetches: 0
Planning Time: 0.063 ms
Execution Time: 0.011 ms
(12 filas)
Usando MySQL o MariaDB, lo siguiente:
Customer.where(id: 1).joins(:orders).explain(:analyze)
produce:
ANALYZE SELECT `shop_accounts`.* FROM `shop_accounts` INNER JOIN `customers` ON `customers`.`id` = `shop_accounts`.`customer_id` WHERE `shop_accounts`.`id` = 1
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | r_rows | filtered | r_filtered | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | no matching row in const table |
+----+-------------+-------+------+---------------+------+---------+------+------+--------+----------+------------+--------------------------------+
1 fila en el conjunto (0.00 seg)
NOTA: Las opciones EXPLAIN y ANALYZE varían según las versiones de MySQL y MariaDB. (MySQL 5.7, MySQL 8.0, MariaDB)
21.2 Interpretación de EXPLAIN
La interpretación de la salida de EXPLAIN está más allá del alcance de esta guía. Los siguientes consejos pueden ser útiles:
SQLite3: EXPLAIN QUERY PLAN
MySQL: EXPLAIN Output Format
MariaDB: EXPLAIN
PostgreSQL: Using EXPLAIN
Comentarios
Se te anima a ayudar a mejorar la calidad de esta guía.
Por favor, contribuye si encuentras algún error tipográfico o factual. Para empezar, puedes leer nuestra contribución a la documentación sección.
También puedes encontrar contenido incompleto o desactualizado. Por favor, añade cualquier documentación faltante para main. Asegúrate de revisar Edge Guides primero para verificar si los problemas ya están resueltos o no en la rama principal. Consulta las Directrices de las Guías de Ruby on Rails para el estilo y las convenciones.
Si por alguna razón encuentras algo que corregir pero no puedes solucionarlo tú mismo, por favor abre un problema.
Y por último, cualquier tipo de discusión sobre la documentación de Ruby on Rails es muy bienvenida en el Foro oficial de Ruby on Rails.