edge
Más en rubyonrails.org: Más Ruby on Rails

Interfaz de consulta de Active Record

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

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

1 ¿Qué es la interfaz de consulta de Active Record?

Si estás acostumbrado a 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

Diagrama de todos los modelos de la librería

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:

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 luego after_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:

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.