1 O que é a Interface de Consulta do Active Record?
Se você está acostumado a usar SQL bruto para encontrar registros de banco de dados, geralmente encontrará melhores maneiras de realizar as mesmas operações no Rails. O Active Record o isola da necessidade de usar SQL na maioria dos casos.
O Active Record executará consultas no banco de dados para você e é compatível com a maioria dos sistemas de banco de dados, incluindo MySQL, MariaDB, PostgreSQL e SQLite. Independentemente do sistema de banco de dados que você está usando, o formato do método Active Record será sempre o mesmo.
Os exemplos de código ao longo deste guia se referirão a um ou mais dos seguintes modelos:
DICA: Todos os modelos a seguir usam id
como chave primária, a menos que especificado de outra forma.
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 do Banco de Dados
Para recuperar objetos do banco de dados, o Active Record fornece vários métodos de busca. Cada método de busca permite que você passe argumentos para realizar determinadas consultas no seu banco de dados sem escrever SQL bruto.
Os métodos são:
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
Métodos de busca que retornam uma coleção, como where
e group
, retornam uma instância de ActiveRecord::Relation
. Métodos que encontram uma única entidade, como find
e first
, retornam uma única instância do modelo.
A operação principal de Model.find(options)
pode ser resumida como:
- Converter as opções fornecidas em uma consulta SQL equivalente.
- Executar a consulta SQL e recuperar os resultados correspondentes do banco de dados.
- Instanciar o objeto Ruby equivalente do modelo apropriado para cada linha resultante.
- Executar os callbacks
after_find
e depoisafter_initialize
, se houver.
2.1 Recuperando um Único Objeto
O Active Record fornece várias maneiras diferentes de recuperar um único objeto.
2.1.1 find
Usando o método find
, você pode recuperar o objeto correspondente à chave primária especificada que corresponda a quaisquer opções fornecidas. Por exemplo:
# Encontre o cliente com a chave primária (id) 10.
irb> customer = Customer.find(10)
=> #<Customer id: 10, first_name: "Ryan">
O equivalente SQL do exemplo acima é:
SELECT * FROM customers WHERE (customers.id = 10) LIMIT 1
O método find
lançará uma exceção ActiveRecord::RecordNotFound
se nenhum registro correspondente for encontrado.
Você também pode usar este método para consultar vários objetos. Chame o método find
e passe um array de chaves primárias. O retorno será um array contendo todos os registros correspondentes às chaves primárias fornecidas. Por exemplo:
```irb
Encontre os clientes com chaves primárias 1 e 10.
irb> customers = Customer.find([1, 10]) # OU Customer.find(1, 10)
=> [#
O equivalente SQL do código acima é:
SELECT * FROM customers WHERE (customers.id IN (1,10))
ATENÇÃO: O método find
lançará uma exceção ActiveRecord::RecordNotFound
a menos que um registro correspondente seja encontrado para todas as chaves primárias fornecidas.
2.1.2 take
O método take
recupera um registro sem nenhuma ordenação implícita. Por exemplo:
irb> customer = Customer.take
=> #<Customer id: 1, first_name: "Lifo">
O equivalente SQL do código acima é:
SELECT * FROM customers LIMIT 1
O método take
retorna nil
se nenhum registro for encontrado e nenhuma exceção será lançada.
Você pode passar um argumento numérico para o método take
para retornar até esse número de resultados. Por exemplo:
irb> customers = Customer.take(2)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 220, first_name: "Sara">]
O equivalente SQL do código acima é:
SELECT * FROM customers LIMIT 2
O método take!
se comporta exatamente como take
, exceto que ele lançará ActiveRecord::RecordNotFound
se nenhum registro correspondente for encontrado.
DICA: O registro recuperado pode variar dependendo do mecanismo de banco de dados.
2.1.3 first
O método first
encontra o primeiro registro ordenado pela chave primária (padrão). Por exemplo:
irb> customer = Customer.first
=> #<Customer id: 1, first_name: "Lifo">
O equivalente SQL do código acima é:
SELECT * FROM customers ORDER BY customers.id ASC LIMIT 1
O método first
retorna nil
se nenhum registro correspondente for encontrado e nenhuma exceção será lançada.
Se o seu escopo padrão contiver um método de ordenação, first
retornará o primeiro registro de acordo com essa ordenação.
Você pode passar um argumento numérico para o método first
para retornar até esse número de resultados. Por exemplo:
irb> customers = Customer.first(3)
=> [#<Customer id: 1, first_name: "Lifo">, #<Customer id: 2, first_name: "Fifo">, #<Customer id: 3, first_name: "Filo">]
O equivalente SQL do código acima é:
SELECT * FROM customers ORDER BY customers.id ASC LIMIT 3
Em uma coleção que é ordenada usando order
, first
retornará o primeiro registro ordenado pelo atributo especificado para order
.
irb> customer = Customer.order(:first_name).first
=> #<Customer id: 2, first_name: "Fifo">
O equivalente SQL do código acima é:
SELECT * FROM customers ORDER BY customers.first_name ASC LIMIT 1
O método first!
se comporta exatamente como first
, exceto que ele lançará ActiveRecord::RecordNotFound
se nenhum registro correspondente for encontrado.
2.1.4 last
O método last
encontra o último registro ordenado pela chave primária (padrão). Por exemplo:
irb> customer = Customer.last
=> #<Customer id: 221, first_name: "Russel">
O equivalente SQL do código acima é:
SELECT * FROM customers ORDER BY customers.id DESC LIMIT 1
O método last
retorna nil
se nenhum registro correspondente for encontrado e nenhuma exceção será lançada.
Se o seu escopo padrão contiver um método de ordenação, last
retornará o último registro de acordo com essa ordenação.
Você pode passar um argumento numérico para o método last
para retornar até esse número de resultados. Por exemplo:
irb> customers = Customer.last(3)
=> [#<Customer id: 219, first_name: "James">, #<Customer id: 220, first_name: "Sara">, #<Customer id: 221, first_name: "Russel">]
O equivalente SQL do código acima é:
SELECT * FROM customers ORDER BY customers.id DESC LIMIT 3
Em uma coleção que é ordenada usando order
, last
retornará o último registro ordenado pelo atributo especificado para order
.
irb> customer = Customer.order(:first_name).last
=> #<Customer id: 220, first_name: "Sara">
O equivalente SQL do código acima é:
SELECT * FROM customers ORDER BY customers.first_name DESC LIMIT 1
O método last!
se comporta exatamente como last
, exceto que ele lançará ActiveRecord::RecordNotFound
se nenhum registro correspondente for encontrado.
2.1.5 find_by
O método find_by
encontra o primeiro registro que corresponde a algumas condições. Por exemplo:
irb> Customer.find_by first_name: 'Lifo'
=> #<Customer id: 1, first_name: "Lifo">
irb> Customer.find_by first_name: 'Jon'
=> nil
É equivalente a escrever:
Customer.where(first_name: 'Lifo').take
O equivalente SQL do código acima é:
SELECT * FROM customers WHERE (customers.first_name = 'Lifo') LIMIT 1
Observe que não há ORDER BY
no SQL acima. Se suas condições find_by
puderem corresponder a vários registros, você deve aplicar uma ordem para garantir um resultado determinístico.
O método find_by!
se comporta exatamente como find_by
, exceto que ele lançará ActiveRecord::RecordNotFound
se nenhum registro correspondente for encontrado. Por exemplo:
irb> Customer.find_by! first_name: 'does not exist'
ActiveRecord::RecordNotFound
Isso é equivalente a escrever:
Customer.where(first_name: 'does not exist').take!
2.2 Recuperando Múltiplos Objetos em Lotes
Muitas vezes, precisamos iterar sobre um grande conjunto de registros, como quando enviamos um boletim informativo para um grande conjunto de clientes ou quando exportamos dados.
Isso pode parecer simples:
# Isso pode consumir muita memória se a tabela for grande.
Customer.all.each do |customer|
NewsMailer.weekly(customer).deliver_now
end
Mas essa abordagem se torna cada vez mais impraticável à medida que o tamanho da tabela aumenta, pois Customer.all.each
instrui o Active Record a buscar a tabela inteira em uma única passagem, construir um objeto de modelo por linha e, em seguida, manter o array inteiro de objetos de modelo na memória. De fato, se tivermos um grande número de registros, a coleção inteira pode exceder a quantidade de memória disponível.
O Rails fornece dois métodos que resolvem esse problema dividindo os registros em lotes amigáveis à memória para processamento. O primeiro método, find_each
, recupera um lote de registros e, em seguida, fornece cada registro para o bloco individualmente como um modelo. O segundo método, find_in_batches
, recupera um lote de registros e, em seguida, fornece o lote inteiro para o bloco como um array de modelos.
DICA: Os métodos find_each
e find_in_batches
são destinados ao processamento em lote de um grande número de registros que não caberiam na memória de uma só vez. Se você apenas precisa percorrer mil registros, os métodos de busca regulares são a opção preferida.
2.2.1 find_each
O método find_each
recupera registros em lotes e, em seguida, fornece cada um para o bloco. No exemplo a seguir, find_each
recupera clientes em lotes de 1000 e os fornece para o bloco um por um:
Customer.find_each do |customer|
NewsMailer.weekly(customer).deliver_now
end
Esse processo é repetido, buscando mais lotes conforme necessário, até que todos os registros tenham sido processados.
find_each
funciona em classes de modelo, como visto acima, e também em relações:
Customer.where(weekly_subscriber: true).find_each do |customer|
NewsMailer.weekly(customer).deliver_now
end
desde que não tenham ordenação, pois o método precisa forçar uma ordem internamente para iterar.
Se uma ordem estiver presente no receptor, o comportamento depende da flag
config.active_record.error_on_ignored_order
. Se for verdadeiro, ArgumentError
é
lançado, caso contrário, a ordem é ignorada e um aviso é emitido, que é o
padrão. Isso pode ser substituído pela opção :error_on_ignore
, explicada
abaixo.
2.2.1.1 Opções para find_each
:batch_size
A opção :batch_size
permite especificar o número de registros a serem recuperados em cada lote, antes de serem passados individualmente para o bloco. Por exemplo, para recuperar registros em lotes de 5000:
Customer.find_each(batch_size: 5000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
:start
Por padrão, os registros são buscados em ordem ascendente da chave primária. A opção :start
permite configurar o primeiro ID da sequência sempre que o ID mais baixo não for o que você precisa. Isso seria útil, por exemplo, se você quisesse retomar um processo em lote interrompido, desde que você tenha salvo o último ID processado como um ponto de verificação.
Por exemplo, para enviar boletins apenas para clientes com a chave primária a partir de 2000:
Customer.find_each(start: 2000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
:finish
Semelhante à opção :start
, :finish
permite configurar o último ID da sequência sempre que o ID mais alto não for o que você precisa.
Isso seria útil, por exemplo, se você quisesse executar um processo em lote usando um subconjunto de registros com base em :start
e :finish
.
Por exemplo, para enviar boletins apenas para clientes com a chave primária a partir de 2000 até 10000:
Customer.find_each(start: 2000, finish: 10000) do |customer|
NewsMailer.weekly(customer).deliver_now
end
Outro exemplo seria se você quisesse que vários trabalhadores manipulassem a mesma
fila de processamento. Você poderia fazer com que cada trabalhador manipulasse 10000 registros, definindo as opções :start
e :finish
apropriadas em cada trabalhador.
:error_on_ignore
Substitui a configuração do aplicativo para especificar se um erro deve ser lançado quando uma ordem está presente na relação.
:order
Especifica a ordem da chave primária (pode ser :asc
ou :desc
). O padrão é :asc
.
ruby
Customer.find_each(order: :desc) do |customer|
NewsMailer.weekly(customer).deliver_now
end
2.2.2 find_in_batches
O método find_in_batches
é semelhante ao find_each
, pois ambos recuperam lotes de registros. A diferença é que find_in_batches
retorna batches para o bloco como um array de modelos, em vez de individualmente. O exemplo a seguir retornará para o bloco fornecido um array de até 1000 clientes por vez, com o último bloco contendo os clientes restantes:
# Dê a add_customers um array de 1000 clientes por vez.
Customer.find_in_batches do |customers|
export.add_customers(customers)
end
find_in_batches
funciona em classes de modelo, como visto acima, e também em relações:
# Dê a add_customers um array de 1000 clientes recentemente ativos por vez.
Customer.recently_active.find_in_batches do |customers|
export.add_customers(customers)
end
desde que não tenham ordenação, pois o método precisa forçar uma ordem internamente para iterar.
2.2.2.1 Opções para find_in_batches
O método find_in_batches
aceita as mesmas opções que find_each
:
:batch_size
Assim como para find_each
, batch_size
estabelece quantos registros serão recuperados em cada grupo. Por exemplo, recuperar lotes de 2500 registros pode ser especificado como:
Customer.find_in_batches(batch_size: 2500) do |customers|
export.add_customers(customers)
end
:start
A opção start
permite especificar o ID inicial de onde os registros serão selecionados. Como mencionado anteriormente, por padrão, os registros são buscados em ordem ascendente da chave primária. Por exemplo, para recuperar clientes a partir do ID: 5000 em lotes de 2500 registros, o seguinte código pode ser usado:
Customer.find_in_batches(batch_size: 2500, start: 5000) do |customers|
export.add_customers(customers)
end
:finish
A opção finish
permite especificar o ID final dos registros a serem recuperados. O código abaixo mostra o caso de recuperar clientes em lotes, até o cliente com ID: 7000:
Customer.find_in_batches(finish: 7000) do |customers|
export.add_customers(customers)
end
:error_on_ignore
A opção error_on_ignore
substitui a configuração do aplicativo para especificar se um erro deve ser gerado quando uma ordem específica está presente na relação.
3 Condições
O método where
permite especificar condições para limitar os registros retornados, representando a parte WHERE
da instrução SQL. As condições podem ser especificadas como uma string, array ou hash.
3.1 Condições de String Pura
Se você deseja adicionar condições à sua consulta, pode especificá-las lá, assim como Book.where("title = 'Introduction to Algorithms'")
. Isso encontrará todos os livros em que o valor do campo title
é 'Introduction to Algorithms'.
ATENÇÃO: Construir suas próprias condições como strings puras pode deixar você vulnerável a ataques de injeção de SQL. Por exemplo, Book.where("title LIKE '%#{params[:title]}%'")
não é seguro. Consulte a próxima seção para saber a maneira preferida de lidar com condições usando um array.
3.2 Condições de Array
Agora, e se esse título pudesse variar, digamos como um argumento de algum lugar? A consulta ficaria assim:
Book.where("title = ?", params[:title])
O ActiveRecord considerará o primeiro argumento como a string de condições e quaisquer argumentos adicionais substituirão os pontos de interrogação (?)
nela.
Se você quiser especificar várias condições:
Book.where("title = ? AND out_of_print = ?", params[:title], false)
Neste exemplo, o primeiro ponto de interrogação será substituído pelo valor em params[:title]
e o segundo será substituído pela representação SQL de false
, que depende do adaptador.
Este código é altamente preferível:
Book.where("title = ?", params[:title])
em relação a este código:
Book.where("title = #{params[:title]}")
por causa da segurança dos argumentos. Colocar a variável diretamente na string de condições passará a variável para o banco de dados como está. Isso significa que será uma variável não escapada diretamente de um usuário que pode ter intenções maliciosas. Se você fizer isso, colocará todo o seu banco de dados em risco, porque, uma vez que um usuário descobrir que pode explorar seu banco de dados, ele poderá fazer qualquer coisa com ele. Nunca coloque seus argumentos diretamente dentro da string de condições.
DICA: Para obter mais informações sobre os perigos da injeção de SQL, consulte o Guia de Segurança do Ruby on Rails.
3.2.1 Condições de Espaço Reservado
Semelhante ao estilo de substituição (?)
de parâmetros, você também pode especificar chaves em sua string de condições junto com um hash de chaves/valores correspondentes:
Book.where("created_at >= :start_date AND created_at <= :end_date",
{ start_date: params[:start_date], end_date: params[:end_date] })
Isso torna a leitura mais clara se você tiver um grande número de condições variáveis.
3.2.2 Condições que Usam LIKE
Embora os argumentos das condições sejam automaticamente escapados para evitar a injeção de SQL, os curingas LIKE
do SQL (ou seja, %
e _
) não são escapados. Isso pode causar comportamento inesperado se um valor não sanitizado for usado em um argumento. Por exemplo:
ruby
Book.order(:title).order(:created_at)
This will generate SQL like this:
SELECT * FROM books ORDER BY title ASC, created_at ASC
You can also use the reorder
method to replace any existing order with a new one:
Book.order(:title).reorder(:created_at)
This will generate SQL like this:
SELECT * FROM books ORDER BY created_at ASC
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 10 books:
Book.limit(10)
This will generate SQL like this:
SELECT * FROM books LIMIT 10
To skip a certain number of records and retrieve the rest, you can use the offset
method. For example, to retrieve books starting from the 11th record:
Book.offset(10)
This will generate SQL like this:
SELECT * FROM books OFFSET 10
You can also chain limit
and offset
together to paginate through records:
Book.limit(10).offset(20)
This will generate SQL like this:
SELECT * FROM books LIMIT 10 OFFSET 20
3.4 Locking Records
Active Record allows you to lock records when querying the database. This can be useful in scenarios where you want to prevent other processes from modifying the same records while you're working with them.
To lock records, you can use 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 directly in a query:
Book.lock.find(1)
This will generate the same SQL as above.
3.5 Selecting Specific Fields
By default, Active Record retrieves all columns from the database table when querying records. However, in some cases, you may only need to retrieve specific fields.
To select specific fields, you can use the select
method. For example, to retrieve only the title
and author
fields from the books table:
Book.select(:title, :author)
This will generate SQL like this:
SELECT title, author FROM books
You can also use the select
method with other query methods:
Book.select(:title, :author).where(out_of_print: true)
This will generate SQL like this:
SELECT title, author FROM books WHERE (books.out_of_print = 1)
3.6 Grouping and Counting
Active Record allows you to group records and perform aggregate functions like counting.
To group records, you can use the group
method. For example, to group books by their author
field:
Book.group(:author)
This will generate SQL like this:
SELECT * FROM books GROUP BY books.author
To perform aggregate functions like counting, you can use the count
method. For example, to count the number of books in each group:
Book.group(:author).count
This will generate SQL like this:
SELECT books.author, COUNT(*) AS count FROM books GROUP BY books.author
You can also use the group
and count
methods together with other query methods:
Book.group(:author).where(out_of_print: true).count
This will generate SQL like this:
SELECT books.author, COUNT(*) AS count FROM books WHERE (books.out_of_print = 1) GROUP BY books.author
3.7 Joins
Active Record allows you to perform joins between tables in the database.
To perform a join, you can use the joins
method. For example, to join the books table with the authors table:
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 the type of join to perform:
Book.joins("INNER JOIN authors ON authors.id = books.author_id")
This will generate the same SQL as above.
3.8 Eager Loading Associations
Active Record allows you to eager load associations to avoid the N+1 query problem.
To eager load associations, you can use the includes
method. For example, to eager load the author association for a set of books:
Book.includes(:author)
This will generate SQL like this:
SELECT * 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 * FROM books LEFT OUTER JOIN authors ON authors.id = books.author_id LEFT OUTER JOIN publishers ON publishers.id = books.publisher_id
3.9 Conclusion
Active Record provides a powerful and flexible query interface for interacting with databases in Ruby on Rails. By understanding and utilizing the various query methods available, you can write efficient and concise database queries for your application.
irb
irb> Book.order("title ASC").order("created_at DESC")
SELECT * FROM books ORDER BY title ASC, created_at DESC
AVISO: Na maioria dos sistemas de banco de dados, ao selecionar campos com distinct
de um conjunto de resultados usando métodos como select
, pluck
e ids
; o método order
lançará uma exceção ActiveRecord::StatementInvalid
a menos que o(s) campo(s) usado(s) na cláusula order
estejam incluídos na lista de seleção. Veja a próxima seção para selecionar campos do conjunto de resultados.
4 Selecionando Campos Específicos
Por padrão, Model.find
seleciona todos os campos do conjunto de resultados usando select *
.
Para selecionar apenas um subconjunto de campos do conjunto de resultados, você pode especificar o subconjunto através do método select
.
Por exemplo, para selecionar apenas as colunas isbn
e out_of_print
:
Book.select(:isbn, :out_of_print)
# OU
Book.select("isbn, out_of_print")
A consulta SQL usada por essa chamada de busca será algo como:
SELECT isbn, out_of_print FROM books
Tenha cuidado, pois isso também significa que você está inicializando um objeto de modelo apenas com os campos que você selecionou. Se você tentar acessar um campo que não está no registro inicializado, você receberá:
ActiveModel::MissingAttributeError: missing attribute '<attribute>' for Book
Onde <attribute>
é o atributo que você solicitou. O método id
não lançará a exceção ActiveRecord::MissingAttributeError
, então tenha cuidado ao trabalhar com associações, pois elas precisam do método id
para funcionar corretamente.
Se você deseja apenas pegar um único registro por valor único em um determinado campo, você pode usar distinct
:
Customer.select(:last_name).distinct
Isso geraria SQL como:
SELECT DISTINCT last_name FROM customers
Você também pode remover a restrição de unicidade:
# Retorna last_names únicos
query = Customer.select(:last_name).distinct
# Retorna todos os last_names, mesmo que haja duplicatas
query.distinct(false)
5 Limite e Deslocamento
Para aplicar o LIMIT
ao SQL disparado pelo Model.find
, você pode especificar o LIMIT
usando os métodos limit
e offset
na relação.
Você pode usar limit
para especificar o número de registros a serem recuperados e usar offset
para especificar o número de registros a serem ignorados antes de começar a retornar os registros. Por exemplo
Customer.limit(5)
retornará no máximo 5 clientes e, como não especifica deslocamento, retornará os primeiros 5 da tabela. O SQL executado será assim:
SELECT * FROM customers LIMIT 5
Adicionando offset
a isso
Customer.limit(5).offset(30)
retornará, em vez disso, no máximo 5 clientes começando pelo 31º. O SQL será assim:
SELECT * FROM customers LIMIT 5 OFFSET 30
6 Agrupamento
Para aplicar uma cláusula GROUP BY
ao SQL disparado pelo localizador, você pode usar o método group
.
Por exemplo, se você quiser encontrar uma coleção das datas em que os pedidos foram criados:
Order.select("created_at").group("created_at")
E isso lhe dará um único objeto Order
para cada data em que houver pedidos no banco de dados.
O SQL que seria executado seria algo como isso:
SELECT created_at
FROM orders
GROUP BY created_at
6.1 Total de Itens Agrupados
Para obter o total de itens agrupados em uma única consulta, chame count
após o group
.
irb> Order.group(:status).count
=> {"being_packed"=>7, "shipped"=>12}
O SQL que seria executado seria algo como isso:
SELECT COUNT (*) AS count_all, status AS status
FROM orders
GROUP BY status
6.2 Condições HAVING
O SQL usa a cláusula HAVING
para especificar condições nos campos GROUP BY
. Você pode adicionar a cláusula HAVING
ao SQL disparado pelo Model.find
adicionando o método having
à busca.
Por exemplo:
Order.select("created_at, sum(total) as total_price").
group("created_at").having("sum(total) > ?", 200)
O SQL que seria executado seria algo como isso:
SELECT created_at as ordered_date, sum(total) as total_price
FROM orders
GROUP BY created_at
HAVING sum(total) > 200
Isso retorna a data e o preço total para cada objeto de pedido, agrupados pelo dia em que foram encomendados e onde o total é superior a $200.
Você pode acessar o total_price
para cada objeto de pedido retornado assim:
big_orders = Order.select("created_at, sum(total) as total_price")
.group("created_at")
.having("sum(total) > ?", 200)
big_orders[0].total_price
# Retorna o preço total para o primeiro objeto de Pedido
7 Substituindo Condições
7.1 unscope
Você pode especificar certas condições a serem removidas usando o método unscope
. Por exemplo:
ruby
Book.where('id > 100').limit(20).order('id desc').unscope(:order)
O SQL que seria executado:
SELECT * FROM books WHERE id > 100 LIMIT 20
-- Consulta original sem `unscope`
SELECT * FROM books WHERE id > 100 ORDER BY id desc LIMIT 20
Você também pode remover cláusulas where
específicas. Por exemplo, isso removerá a condição id
da cláusula where:
Book.where(id: 10, out_of_print: false).unscope(where: :id)
# SELECT books.* FROM books WHERE out_of_print = 0
Uma relação que usou unscope
afetará qualquer relação na qual ela é mesclada:
Book.order('id desc').merge(Book.unscope(:order))
# SELECT books.* FROM books
7.2 only
Você também pode substituir condições usando o método only
. Por exemplo:
Book.where('id > 10').limit(20).order('id desc').only(:order, :where)
O SQL que seria executado:
SELECT * FROM books WHERE id > 10 ORDER BY id DESC
-- Consulta original sem `only`
SELECT * FROM books WHERE id > 10 ORDER BY id DESC LIMIT 20
7.3 reselect
O método reselect
substitui uma declaração de seleção existente. Por exemplo:
Book.select(:title, :isbn).reselect(:created_at)
O SQL que seria executado:
SELECT books.created_at FROM books
Compare isso com o caso em que a cláusula reselect
não é usada:
Book.select(:title, :isbn).select(:created_at)
o SQL executado seria:
SELECT books.title, books.isbn, books.created_at FROM books
7.4 reorder
O método reorder
substitui a ordem do escopo padrão. Por exemplo, se a definição da classe incluir isso:
class Author < ApplicationRecord
has_many :books, -> { order(year_published: :desc) }
end
E você executar isso:
Author.find(10).books
O SQL que seria executado:
SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published DESC
Você pode usar a cláusula reorder
para especificar uma maneira diferente de ordenar os livros:
Author.find(10).books.reorder('year_published ASC')
O SQL que seria executado:
SELECT * FROM authors WHERE id = 10 LIMIT 1
SELECT * FROM books WHERE author_id = 10 ORDER BY year_published ASC
7.5 reverse_order
O método reverse_order
inverte a cláusula de ordenação, se especificada.
Book.where("author_id > 10").order(:year_published).reverse_order
O SQL que seria executado:
SELECT * FROM books WHERE author_id > 10 ORDER BY year_published DESC
Se nenhuma cláusula de ordenação for especificada na consulta, o reverse_order
ordena pela chave primária em ordem reversa.
Book.where("author_id > 10").reverse_order
O SQL que seria executado:
SELECT * FROM books WHERE author_id > 10 ORDER BY books.id DESC
O método reverse_order
não aceita nenhum argumento.
7.6 rewhere
O método [rewhere
][] substitui uma condição where
nomeada existente. Por exemplo:
Book.where(out_of_print: true).rewhere(out_of_print: false)
O SQL que seria executado:
SELECT * FROM books WHERE out_of_print = 0
Se a cláusula rewhere
não for usada, as cláusulas where são combinadas com AND:
Book.where(out_of_print: true).where(out_of_print: false)
o SQL executado seria:
SELECT * FROM books WHERE out_of_print = 1 AND out_of_print = 0
7.7 regroup
O método regroup
substitui uma condição group
nomeada existente. Por exemplo:
Book.group(:author).regroup(:id)
O SQL que seria executado:
SELECT * FROM books GROUP BY id
Se a cláusula regroup
não for usada, as cláusulas de grupo são combinadas:
Book.group(:author).group(:id)
o SQL executado seria:
SELECT * FROM books GROUP BY author, id
8 Relação Nula
O método none
retorna uma relação encadeável sem registros. Quaisquer condições subsequentes encadeadas à relação retornada continuarão gerando relações vazias. Isso é útil em cenários em que você precisa de uma resposta encadeável para um método ou escopo que pode retornar zero resultados.
Book.none # retorna uma Relação vazia e não executa consultas.
# O método highlighted_reviews abaixo deve sempre retornar uma Relação.
Book.first.highlighted_reviews.average(:rating)
# => Retorna a média de avaliação de um livro
class Book
# Retorna as avaliações se houver pelo menos 5,
# caso contrário, considere este como um livro não avaliado
def highlighted_reviews
if reviews.count > 5
reviews
else
Review.none # Ainda não atende ao limite mínimo
end
end
end
9 Objetos Somente Leitura
O Active Record fornece o método readonly
em uma relação para explicitamente impedir a modificação de qualquer um dos objetos retornados. Qualquer tentativa de alterar um registro somente leitura não terá sucesso, levantando uma exceção ActiveRecord::ReadOnlyRecord
.
ruby
customer = Customer.readonly.first
customer.visits += 1
customer.save
Como o objeto customer
está explicitamente definido como somente leitura, o código acima irá gerar uma exceção ActiveRecord::ReadOnlyRecord
ao chamar customer.save
com um valor atualizado para visits.
10 Bloqueando Registros para Atualização
Bloquear registros é útil para evitar condições de corrida ao atualizar registros no banco de dados e garantir atualizações atômicas.
O Active Record fornece dois mecanismos de bloqueio:
- Bloqueio Otimista
- Bloqueio Pessimista
10.1 Bloqueio Otimista
O bloqueio otimista permite que vários usuários acessem o mesmo registro para edições e pressupõe um mínimo de conflitos com os dados. Ele faz isso verificando se outro processo fez alterações em um registro desde que ele foi aberto. Uma exceção ActiveRecord::StaleObjectError
é lançada se isso ocorrer e a atualização é ignorada.
Coluna de bloqueio otimista
Para usar o bloqueio otimista, a tabela precisa ter uma coluna chamada lock_version
do tipo inteiro. Sempre que o registro é atualizado, o Active Record incrementa a coluna lock_version
. Se uma solicitação de atualização for feita com um valor menor no campo lock_version
do que o valor atualmente na coluna lock_version
no banco de dados, a solicitação de atualização falhará com uma exceção ActiveRecord::StaleObjectError
.
Por exemplo:
c1 = Customer.find(1)
c2 = Customer.find(1)
c1.first_name = "Sandra"
c1.save
c2.first_name = "Michael"
c2.save # Gera uma exceção ActiveRecord::StaleObjectError
Você é responsável por lidar com o conflito, resgatando a exceção e fazendo rollback, mesclando ou aplicando a lógica de negócios necessária para resolver o conflito.
Esse comportamento pode ser desativado definindo ActiveRecord::Base.lock_optimistically = false
.
Para substituir o nome da coluna lock_version
, ActiveRecord::Base
fornece um atributo de classe chamado locking_column
:
class Customer < ApplicationRecord
self.locking_column = :lock_customer_column
end
10.2 Bloqueio Pessimista
O bloqueio pessimista usa um mecanismo de bloqueio fornecido pelo banco de dados subjacente. Usar lock
ao construir uma relação obtém um bloqueio exclusivo nas linhas selecionadas. As relações que usam lock
geralmente são envolvidas em uma transação para evitar condições de deadlock.
Por exemplo:
Book.transaction do
book = Book.lock.first
book.title = 'Algorithms, second edition'
book.save!
end
A sessão acima produz a seguinte SQL para um banco de dados 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
Você também pode passar SQL bruto para o método lock
para permitir diferentes tipos de bloqueios. Por exemplo, o MySQL tem uma expressão chamada LOCK IN SHARE MODE
onde você pode bloquear um registro, mas ainda permitir que outras consultas o leiam. Para especificar essa expressão, basta passá-la como opção de bloqueio:
Book.transaction do
book = Book.lock("LOCK IN SHARE MODE").find(1)
book.increment!(:views)
end
NOTA: Observe que seu banco de dados deve suportar o SQL bruto que você passa para o método lock
.
Se você já tem uma instância do seu modelo, pode iniciar uma transação e adquirir o bloqueio de uma vez usando o seguinte código:
book = Book.first
book.with_lock do
# Este bloco é chamado dentro de uma transação,
# o livro já está bloqueado.
book.increment!(:views)
end
11 Unindo Tabelas
O Active Record fornece dois métodos de busca para especificar cláusulas JOIN
no SQL resultante: joins
e left_outer_joins
.
Enquanto joins
deve ser usado para INNER JOIN
ou consultas personalizadas,
left_outer_joins
é usado para consultas usando LEFT OUTER JOIN
.
11.1 joins
Existem várias maneiras de usar o método joins
.
11.1.1 Usando um Fragmento de SQL em String
Você pode simplesmente fornecer o SQL bruto especificando a cláusula JOIN
para joins
:
Author.joins("INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE")
Isso resultará no seguinte SQL:
SELECT authors.* FROM authors INNER JOIN books ON books.author_id = authors.id AND books.out_of_print = FALSE
11.1.2 Usando Array/Hash de Associações Nomeadas
O Active Record permite que você use os nomes das associações definidas no modelo como um atalho para especificar cláusulas JOIN
para essas associações ao usar o método joins
.
Todas as seguintes produzirão as consultas de junção esperadas usando INNER JOIN
:
11.1.2.1 Unindo uma Única Associação
Book.joins(:reviews)
Isso produz:
SELECT books.* FROM books
INNER JOIN reviews ON reviews.book_id = books.id
Ou, em português: "retorne um objeto Book para todos os livros com avaliações". Observe que você verá livros duplicados se um livro tiver mais de uma avaliação. Se você quiser livros únicos, pode usar Book.joins(:reviews).distinct
.
11.1.3 Unindo Múltiplas Associações
Book.joins(:author, :reviews)
Isso produz:
SELECT books.* FROM books
INNER JOIN authors ON authors.id = books.author_id
INNER JOIN reviews ON reviews.book_id = books.id
Ou, em inglês: "retorne todos os livros com seus autores que possuem pelo menos uma avaliação". Note novamente que os livros com várias avaliações aparecerão várias vezes.
11.1.3.1 Unindo Associações Aninhadas (Nível Único)
Book.joins(reviews: :customer)
Isso produz:
SELECT books.* FROM books
INNER JOIN reviews ON reviews.book_id = books.id
INNER JOIN customers ON customers.id = reviews.customer_id
Ou, em inglês: "retorne todos os livros que possuem uma avaliação feita por um cliente".
11.1.3.2 Unindo Associações Aninhadas (Múltiplos Níveis)
Author.joins(books: [{ reviews: { customer: :orders } }, :supplier])
Isso produz:
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
Ou, em inglês: "retorne todos os autores que possuem livros com avaliações e foram encomendados por um cliente, e os fornecedores desses livros".
11.1.4 Especificando Condições nas Tabelas Unidas
Você pode especificar condições nas tabelas unidas usando as condições regulares de Array e String. As condições de Hash fornecem uma sintaxe especial para especificar condições para as tabelas unidas:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where('orders.created_at' => time_range).distinct
Isso encontrará todos os clientes que possuem pedidos criados ontem, usando uma expressão SQL BETWEEN
para comparar created_at
.
Uma sintaxe alternativa e mais limpa é aninhar as condições de hash:
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
Customer.joins(:orders).where(orders: { created_at: time_range }).distinct
Para condições mais avançadas ou para reutilizar um escopo nomeado existente, merge
pode ser usado. Primeiro, vamos adicionar um novo escopo nomeado ao modelo Order
:
class Order < ApplicationRecord
belongs_to :customer
scope :created_in_time_range, ->(time_range) {
where(created_at: time_range)
}
end
Agora podemos usar merge
para mesclar o escopo 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
Isso encontrará todos os clientes que possuem pedidos criados ontem, novamente usando uma expressão SQL BETWEEN
.
11.2 left_outer_joins
Se você deseja selecionar um conjunto de registros, independentemente de eles terem registros associados ou não, você pode usar o método left_outer_joins
.
Customer.left_outer_joins(:reviews).distinct.select('customers.*, COUNT(reviews.*) AS reviews_count').group('customers.id')
O que produz:
SELECT DISTINCT customers.*, COUNT(reviews.*) AS reviews_count FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id GROUP BY customers.id
O que significa: "retorne todos os clientes com a contagem de suas avaliações, independentemente de eles terem ou não avaliações".
11.3 where.associated
e where.missing
Os métodos de consulta associated
e missing
permitem selecionar um conjunto de registros com base na presença ou ausência de uma associação.
Para usar where.associated
:
Customer.where.associated(:reviews)
Produz:
SELECT customers.* FROM customers
INNER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NOT NULL
O que significa "retorne todos os clientes que fizeram pelo menos uma avaliação".
Para usar where.missing
:
Customer.where.missing(:reviews)
Produz:
SELECT customers.* FROM customers
LEFT OUTER JOIN reviews ON reviews.customer_id = customers.id
WHERE reviews.id IS NULL
O que significa "retorne todos os clientes que não fizeram nenhuma avaliação".
12 Carregamento Antecipado de Associações
O carregamento antecipado é o mecanismo para carregar os registros associados aos objetos retornados por Model.find
usando o menor número possível de consultas.
12.1 Problema de Consultas N + 1
Considere o seguinte código, que encontra 10 livros e imprime o sobrenome de seus autores:
books = Book.limit(10)
books.each do |book|
puts book.author.last_name
end
Este código parece bom à primeira vista. Mas o problema está no número total de consultas executadas. O código acima executa 1 (para encontrar 10 livros) + 10 (uma para cada livro para carregar o autor) = 11 consultas no total.
12.1.1 Solução para o Problema de Consultas N + 1
O Active Record permite que você especifique antecipadamente todas as associações que serão carregadas.
Os métodos são:
12.2 includes
Com includes
, o Active Record garante que todas as associações especificadas sejam carregadas usando o menor número possível de consultas.
Revisitando o caso acima usando o método includes
, poderíamos reescrever Book.limit(10)
para carregar antecipadamente os autores:
books = Book.includes(:author).limit(10)
books.each do |book|
puts book.author.last_name
end
O código acima executará apenas 2 consultas, em oposição às 11 consultas do 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 Carregamento Antecipado de Múltiplas Associações
O Active Record permite carregar antecipadamente qualquer número de associações com uma única chamada Model.find
usando um array, hash ou um hash aninhado de array/hash com o método includes
.
12.2.1.1 Array de Múltiplas Associações
Customer.includes(:orders, :reviews)
Isso carrega todos os clientes e os pedidos e avaliações associados para cada um.
12.2.1.2 Hash de Associações Aninhadas
Customer.includes(orders: { books: [:supplier, :author] }).find(1)
Isso encontrará o cliente com o id 1 e carregará antecipadamente todos os pedidos associados a ele, os livros de todos os pedidos e o autor e fornecedor de cada livro.
12.2.2 Especificando Condições em Associações Carregadas Antecipadamente
Embora o Active Record permita especificar condições nas associações carregadas antecipadamente, assim como joins
, a maneira recomendada é usar joins em vez disso.
No entanto, se você precisar fazer isso, poderá usar where
como faria normalmente.
Author.includes(:books).where(books: { out_of_print: true })
Isso geraria uma consulta que contém um LEFT OUTER JOIN
, enquanto o método joins
geraria um usando a função INNER JOIN
em vez disso.
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)
Se não houver condição where
, isso gerará o conjunto normal de duas consultas.
NOTA: Usar where
dessa forma só funcionará quando você passar um Hash. Para
fragmentos SQL, você precisa usar references
para forçar a junção de tabelas:
Author.includes(:books).where("books.out_of_print = true").references(:books)
Se, no caso dessa consulta includes
, não houver livros para nenhum
autor, todos os autores ainda serão carregados. Usando joins
(um INNER
JOIN), as condições de junção devem corresponder, caso contrário, nenhum registro será
retornado.
NOTA: Se uma associação for carregada antecipadamente como parte de uma junção, nenhum campo de uma cláusula de seleção personalizada estará presente nos modelos carregados. Isso ocorre porque é ambíguo se eles devem aparecer no registro pai ou no filho.
12.3 preload
Com preload
, o Active Record carrega cada associação especificada usando uma consulta por associação.
Revisitando o problema das consultas N + 1, poderíamos reescrever Book.limit(10)
para carregar antecipadamente os autores:
books = Book.preload(:author).limit(10)
books.each do |book|
puts book.author.last_name
end
O código acima executará apenas 2 consultas, em oposição às 11 consultas do 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: O método preload
usa um array, hash ou um hash aninhado de array/hash da mesma forma que o método includes
para carregar qualquer número de associações com uma única chamada Model.find
. No entanto, ao contrário do método includes
, não é possível especificar condições para associações carregadas antecipadamente.
12.4 eager_load
Com eager_load
, o Active Record carrega todas as associações especificadas usando um LEFT OUTER JOIN
.
Revisitando o caso em que ocorreu N + 1 usando o método eager_load
, poderíamos reescrever Book.limit(10)
para carregar antecipadamente os autores:
books = Book.eager_load(:author).limit(10)
books.each do |book|
puts book.author.last_name
end
O código acima executará apenas 2 consultas, em oposição às 11 consultas do 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: O método eager_load
usa um array, hash ou um hash aninhado de array/hash da mesma forma que o método includes
para carregar qualquer número de associações com uma única chamada Model.find
. Além disso, como o método includes
, você pode especificar condições para associações carregadas antecipadamente.
12.5 strict_loading
O carregamento antecipado pode evitar consultas N + 1, mas você ainda pode estar carregando preguiçosamente
algumas associações. Para garantir que nenhuma associação seja carregada preguiçosamente, você pode ativar
strict_loading
.
Ao ativar o modo de carregamento estrito em uma relação, um
ActiveRecord::StrictLoadingViolationError
será lançado se o registro tentar
carregar preguiçosamente uma associação:
user = User.strict_loading.first
user.comments.to_a # levanta um ActiveRecord::StrictLoadingViolationError
13 Escopos
O escopo permite que você especifique consultas comumente usadas que podem ser referenciadas como chamadas de método nos objetos de associação ou modelos. Com esses escopos, você pode usar todos os métodos previamente abordados, como where
, joins
e includes
. Todos os corpos de escopo devem retornar um ActiveRecord::Relation
ou nil
para permitir a chamada de outros métodos (como outros escopos) nele.
Para definir um escopo simples, usamos o método scope
dentro da classe, passando a consulta que gostaríamos de executar quando esse escopo for chamado:
class Book < ApplicationRecord
scope :out_of_print, -> { where(out_of_print: true) }
end
Para chamar esse escopo out_of_print
, podemos chamá-lo na classe:
irb> Book.out_of_print
=> #<ActiveRecord::Relation> # todos os livros fora de catálogo
Ou em uma associação composta por objetos Book
:
irb> author = Author.first
irb> author.books.out_of_print
=> #<ActiveRecord::Relation> # todos os livros fora de catálogo do `author`
Os escopos também podem ser encadeados dentro de escopos:
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 Passando Argumentos
Seu escopo pode receber argumentos:
class Book < ApplicationRecord
scope :costs_more_than, ->(amount) { where("price > ?", amount) }
end
Chame o escopo como se fosse um método de classe:
irb> Book.costs_more_than(100.10)
No entanto, isso está apenas duplicando a funcionalidade que seria fornecida a você por um método de classe.
class Book < ApplicationRecord
def self.costs_more_than(amount)
where("price > ?", amount)
end
end
Esses métodos ainda estarão acessíveis nos objetos de associação:
irb> author.books.costs_more_than(100.10)
13.2 Usando Condicionais
Seu escopo pode utilizar condicionais:
class Order < ApplicationRecord
scope :created_before, ->(time) { where(created_at: ...time) if time.present? }
end
Assim como nos outros exemplos, isso se comportará de forma semelhante a um método de classe.
class Order < ApplicationRecord
def self.created_before(time)
where(created_at: ...time) if time.present?
end
end
No entanto, há uma ressalva importante: um escopo sempre retornará um objeto ActiveRecord::Relation
, mesmo se a condicional for avaliada como false
, enquanto um método de classe retornará nil
. Isso pode causar um NoMethodError
ao encadear métodos de classe com condicionais, se alguma das condicionais retornar false
.
13.3 Aplicando um Escopo Padrão
Se desejarmos que um escopo seja aplicado a todas as consultas do modelo, podemos usar o método default_scope
dentro do próprio modelo.
class Book < ApplicationRecord
default_scope { where(out_of_print: false) }
end
Quando as consultas são executadas neste modelo, a consulta SQL agora será algo como:
SELECT * FROM books WHERE (out_of_print = false)
Se você precisar fazer coisas mais complexas com um escopo padrão, você pode, alternativamente, defini-lo como um método de classe:
class Book < ApplicationRecord
def self.default_scope
# Deve retornar um ActiveRecord::Relation.
end
end
NOTA: O default_scope
também é aplicado ao criar/construir um registro quando os argumentos do escopo são fornecidos como um Hash
. Ele não é aplicado ao atualizar um registro. Por exemplo:
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>
Esteja ciente de que, quando fornecido no formato Array
, os argumentos da consulta default_scope
não podem ser convertidos em um Hash
para atribuição de atributo padrão. Por exemplo:
class Book < ApplicationRecord
default_scope { where("out_of_print = ?", false) }
end
irb> Book.new
=> #<Book id: nil, out_of_print: nil>
13.4 Mesclando Escopos
Assim como as cláusulas where
, os escopos são mesclados usando condições 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 misturar e combinar condições scope
e where
e a consulta SQL final terá todas as condições unidas com AND
.
irb> Book.in_print.where(price: ...100)
SELECT books.* FROM books WHERE books.out_of_print = 'false' AND books.price < 100
Se quisermos que a última cláusula where
prevaleça, então merge
pode ser usado.
irb> Book.in_print.merge(Book.out_of_print)
SELECT books.* FROM books WHERE books.out_of_print = true
Uma ressalva importante é que o default_scope
será adicionado antes das condições scope
e 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 você pode ver acima, o default_scope
está sendo mesclado nas condições de ambos os scope
e where
.
13.5 Removendo todos os escopos
Se desejarmos remover os escopos por qualquer motivo, podemos usar o método unscoped
. Isso é
especialmente útil se um default_scope
for especificado no modelo e não deve ser
aplicado para esta consulta específica.
Book.unscoped.load
Este método remove todos os escopos e fará uma consulta normal na tabela.
irb> Book.unscoped.all
SELECT books.* FROM books
irb> Book.where(out_of_print: true).unscoped.all
SELECT books.* FROM books
unscoped
também pode aceitar um bloco:
irb> Book.unscoped { Book.out_of_print }
SELECT books.* FROM books WHERE books.out_of_print
14 Finders Dinâmicos
Para cada campo (também conhecido como atributo) definido em sua tabela,
Active Record fornece um método de busca. Se você tiver um campo chamado first_name
em seu modelo Customer
, por exemplo,
você obtém o método de instância find_by_first_name
gratuitamente do Active Record.
Se você também tiver um campo locked
no modelo Customer
, você também obtém o método find_by_locked
.
Você pode especificar um ponto de exclamação (!
) no final dos finders dinâmicos
para fazer com que eles levantem um erro ActiveRecord::RecordNotFound
se eles não retornarem nenhum registro, como Customer.find_by_first_name!("Ryan")
Se você quiser encontrar tanto por first_name
quanto por orders_count
, você pode encadear esses finders simplesmente digitando "and
" entre os campos.
Por exemplo, Customer.find_by_first_name_and_orders_count("Ryan", 5)
.
15 Enums
Um enum permite que você defina um array de valores para um atributo e se refira a eles pelo nome. O valor real armazenado no banco de dados é um inteiro que foi mapeado para um dos valores.
Declarar um enum irá:
- Criar escopos que podem ser usados para encontrar todos os objetos que têm ou não têm um dos valores do enum
- Criar um método de instância que pode ser usado para determinar se um objeto tem um valor específico para o enum
- Criar um método de instância que pode ser usado para alterar o valor do enum de um objeto
para todos os valores possíveis de um enum.
Por exemplo, dado esta declaração enum
:
class Order < ApplicationRecord
enum :status, [:shipped, :being_packaged, :complete, :cancelled]
end
Esses escopos são criados automaticamente e podem ser usados para encontrar todos os objetos com ou sem um valor específico para status
:
irb> Order.shipped
=> #<ActiveRecord::Relation> # todos os pedidos com status == :shipped
irb> Order.not_shipped
=> #<ActiveRecord::Relation> # todos os pedidos com status != :shipped
Esses métodos de instância são criados automaticamente e consultam se o modelo possui esse valor para o enum status
:
irb> order = Order.shipped.first
irb> order.shipped?
=> true
irb> order.complete?
=> false
Esses métodos de instância são criados automaticamente e primeiro atualizam o valor de status
para o valor nomeado
e, em seguida, consultam se o status foi definido com sucesso para o 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
A documentação completa sobre enums pode ser encontrada aqui.
16 Entendendo o Encadeamento de Métodos
O padrão Active Record implementa o Encadeamento de Métodos, que nos permite usar vários métodos do Active Record juntos de maneira simples e direta.
Você pode encadear métodos em uma instrução quando o método anterior chamado retorna um
ActiveRecord::Relation
, como all
, where
e joins
. Métodos que retornam
um único objeto (veja a seção Recuperando um Único Objeto)
devem estar no final da instrução.
Abaixo estão alguns exemplos. Este guia não cobrirá todas as possibilidades, apenas algumas como exemplos. Quando um método Active Record é chamado, a consulta não é gerada imediatamente e enviada para o banco de dados. A consulta é enviada apenas quando os dados são realmente necessários. Portanto, cada exemplo abaixo gera uma única consulta.
16.1 Recuperando Dados Filtrados de Múltiplas Tabelas
Customer
.select('customers.id, customers.last_name, reviews.body')
.joins(:reviews)
.where('reviews.created_at > ?', 1.week.ago)
O resultado deve ser algo como:
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 Dados Específicos de Múltiplas Tabelas
Book
.select('books.id, books.title, authors.first_name')
.joins(:author)
.find_by(title: 'Abstraction and Specification in Program Development')
O código acima deve gerar:
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: Note que se uma consulta corresponder a vários registros, find_by
irá buscar apenas o primeiro e ignorar os outros (veja a declaração LIMIT 1
acima).
17 Encontrar ou Criar um Novo Objeto
É comum que você precise encontrar um registro ou criá-lo se ele não existir. Você pode fazer isso usando os métodos find_or_create_by
e find_or_create_by!
.
17.1 find_or_create_by
O método find_or_create_by
verifica se um registro com os atributos especificados existe. Se não existir, então o método create
é chamado. Vejamos um exemplo.
Suponha que você queira encontrar um cliente chamado "Andy" e, se não houver nenhum, criar um. Você pode fazer isso executando:
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">
O SQL gerado por este método é semelhante a isso:
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
retorna o registro que já existe ou o novo registro. No nosso caso, não tínhamos um cliente chamado Andy, então o registro é criado e retornado.
O novo registro pode não ser salvo no banco de dados; isso depende se as validações foram aprovadas ou não (assim como create
).
Suponha que queremos definir o atributo 'locked' como false
se estivermos criando um novo registro, mas não queremos incluí-lo na consulta. Então, queremos encontrar o cliente chamado "Andy" ou, se esse cliente não existir, criar um cliente chamado "Andy" que não esteja bloqueado.
Podemos fazer isso de duas maneiras. A primeira é usar create_with
:
Customer.create_with(locked: false).find_or_create_by(first_name: 'Andy')
A segunda maneira é usar um bloco:
Customer.find_or_create_by(first_name: 'Andy') do |c|
c.locked = false
end
O bloco só será executado se o cliente estiver sendo criado. Na segunda vez que executarmos este código, o bloco será ignorado.
17.2 find_or_create_by!
Você também pode usar find_or_create_by!
para lançar uma exceção se o novo registro for inválido. As validações não são abordadas neste guia, mas vamos supor por um momento que você temporariamente adicione
validates :orders_count, presence: true
ao seu modelo Customer
. Se você tentar criar um novo Customer
sem passar um orders_count
, o registro será inválido e uma exceção será lançada:
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
O método find_or_initialize_by
funcionará da mesma forma que find_or_create_by
, mas ele chamará new
em vez de create
. Isso significa que uma nova instância do modelo será criada na memória, mas não será salva no banco de dados. Continuando com o exemplo do find_or_create_by
, agora queremos o cliente chamado '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 o objeto ainda não está armazenado no banco de dados, o SQL gerado é assim:
SELECT * FROM customers WHERE (customers.first_name = 'Nina') LIMIT 1
Quando você quiser salvá-lo no banco de dados, basta chamar save
:
irb> nina.save
=> true
18 Encontrando por SQL
Se você quiser usar seu próprio SQL para encontrar registros em uma tabela, pode usar find_by_sql
. O método find_by_sql
retornará um array de objetos, mesmo que a consulta subjacente retorne apenas um único registro. Por exemplo, você pode executar 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
fornece uma maneira simples de fazer chamadas personalizadas ao banco de dados e recuperar objetos instanciados.
18.1 select_all
find_by_sql
tem um parente próximo chamado connection.select_all
. select_all
irá recuperar
objetos do banco de dados usando SQL personalizado, assim como find_by_sql
, mas não os instanciará.
Este método retornará uma instância da classe ActiveRecord::Result
e chamar to_a
neste
objeto retornará um array de hashes onde cada hash indica um 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
pode ser usado para selecionar o(s) valor(es) da(s) coluna(s) nomeada(s) na relação atual. Ele aceita uma lista de nomes de colunas como argumento e retorna um array de valores das colunas especificadas com o tipo de dados correspondente.
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 substituir código como:
Customer.select(:id).map { |c| c.id }
# ou
Customer.select(:id).map(&:id)
# ou
Customer.select(:id, :first_name).map { |c| [c.id, c.first_name] }
por:
Customer.pluck(:id)
# ou
Customer.pluck(:id, :first_name)
Ao contrário de select
, pluck
converte diretamente um resultado do banco de dados em um Array
do Ruby,
sem construir objetos ActiveRecord
. Isso pode significar melhor desempenho para
uma consulta grande ou frequentemente executada. No entanto, qualquer substituição de método do modelo
não estará disponível. Por exemplo:
class Customer < ApplicationRecord
def name
"Eu sou #{first_name}"
end
end
irb> Customer.select(:first_name).map &:name
=> ["Eu sou David", "Eu sou Jeremy", "Eu sou Jose"]
irb> Customer.pluck(:first_name)
=> ["David", "Jeremy", "Jose"]
Você não está limitado a consultar campos de uma única tabela, também pode consultar várias tabelas.
irb> Order.joins(:customer, :books).pluck("orders.created_at, customers.email, books.title")
Além disso, ao contrário de select
e outros escopos de Relation
, pluck
dispara uma consulta imediata
e, portanto, não pode ser encadeado com outros escopos, embora possa funcionar com
escopos já construídos anteriormente:
irb> Customer.pluck(:first_name).limit(1)
NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
irb> Customer.limit(1).pluck(:first_name)
=> ["David"]
NOTA: Você também deve saber que o uso de pluck
irá disparar o carregamento antecipado se o objeto de relação contiver valores de inclusão, mesmo que o carregamento antecipado não seja necessário para a consulta. Por exemplo:
irb> assoc = Customer.includes(:reviews)
irb> assoc.pluck(:id)
SELECT "customers"."id" FROM "customers" LEFT OUTER JOIN "reviews" ON "reviews"."id" = "customers"."review_id"
Uma maneira de evitar isso é desfazer
as inclusões:
irb> assoc.unscope(:includes).pluck(:id)
18.3 pick
pick
pode ser usado para selecionar o(s) valor(es) da(s) coluna(s) nomeada(s) na relação atual. Ele aceita uma lista de nomes de colunas como argumento e retorna a primeira linha dos valores da coluna especificada com o tipo de dados correspondente.
pick
é uma forma abreviada de relation.limit(1).pluck(*column_names).first
, que é principalmente útil quando você já tem uma relação limitada a uma linha.
pick
permite substituir código como:
Customer.where(id: 1).pluck(:id).first
por:
Customer.where(id: 1).pick(:id)
18.4 ids
ids
pode ser usado para selecionar todos os IDs da relação usando a chave primária da tabela.
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 Existência de Objetos
Se você simplesmente deseja verificar a existência do objeto, há um método chamado exists?
. Este método consultará o banco de dados usando a mesma consulta que find
, mas em vez de retornar um
objeto ou coleção de objetos, ele retornará true
ou false
.
Customer.exists?(1)
O método exists?
também aceita vários valores, mas a condição é que ele retornará true
se algum
desses registros existir.
Customer.exists?(id: [1, 2, 3])
# ou
Customer.exists?(first_name: ['Jane', 'Sergei'])
Também é possível usar exists?
sem argumentos em um modelo ou relação.
Customer.where(first_name: 'Ryan').exists?
O exemplo acima retorna true
se houver pelo menos um cliente com o first_name
'Ryan' e false
caso contrário.
Customer.exists?
O exemplo acima retorna false
se a tabela customers
estiver vazia e true
caso contrário.
Você também pode usar any?
e many?
para verificar a existência em um modelo ou relação. many?
usará o SQL count
para determinar se o item existe.
```ruby
via a model
Order.any?
SELECT 1 FROM orders LIMIT 1
Order.many?
SELECT COUNT(*) FROM (SELECT 1 FROM orders LIMIT 2)
via a named scope
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)
via a relation
Book.where(out_of_print: true).any? Book.where(out_of_print: true).many?
via an association
Customer.first.orders.any? Customer.first.orders.many? ```
20 Cálculos
Esta seção usa o método count
como exemplo neste preâmbulo, mas as opções descritas se aplicam a todas as subseções.
Todos os métodos de cálculo funcionam diretamente em um modelo:
irb> Customer.count
SELECT COUNT(*) FROM customers
Ou em uma relação:
irb> Customer.where(first_name: 'Ryan').count
SELECT COUNT(*) FROM customers WHERE (first_name = 'Ryan')
Você também pode usar vários métodos de busca em uma relação para realizar cálculos complexos:
irb> Customer.includes("orders").where(first_name: 'Ryan', orders: { status: 'shipped' }).count
Que executará:
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)
supondo que Order tenha enum status: [ :shipped, :being_packed, :cancelled ]
.
20.1 count
Se você quiser ver quantos registros existem na tabela do seu modelo, pode chamar Customer.count
e isso retornará o número.
Se você quiser ser mais específico e encontrar todos os clientes com um título presente no banco de dados, pode usar Customer.count(:title)
.
Para opções, consulte a seção pai, Cálculos.
20.2 average
Se você quiser ver a média de um determinado número em uma de suas tabelas, pode chamar o método average
na classe que se relaciona com a tabela. Essa chamada de método será algo como:
Order.average("subtotal")
Isso retornará um número (possivelmente um número de ponto flutuante, como 3.14159265) representando o valor médio no campo.
Para opções, consulte a seção pai, Cálculos.
20.3 minimum
Se você quiser encontrar o valor mínimo de um campo em sua tabela, pode chamar o método minimum
na classe que se relaciona com a tabela. Essa chamada de método será algo como:
Order.minimum("subtotal")
Para opções, consulte a seção pai, Cálculos.
20.4 maximum
Se você quiser encontrar o valor máximo de um campo em sua tabela, pode chamar o método maximum
na classe que se relaciona com a tabela. Essa chamada de método será algo como:
Order.maximum("subtotal")
Para opções, consulte a seção pai, Cálculos.
20.5 sum
Se você quiser encontrar a soma de um campo para todos os registros em sua tabela, pode chamar o método sum
na classe que se relaciona com a tabela. Essa chamada de método será algo como:
Order.sum("subtotal")
Para opções, consulte a seção pai, Cálculos.
21 Executando EXPLAIN
Você pode executar explain
em uma relação. A saída do EXPLAIN varia para cada banco de dados.
Por exemplo, executar
Customer.where(id: 1).joins(:orders).explain
pode resultar em
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)
no MySQL e MariaDB.
O Active Record realiza uma impressão bonita que emula a do shell do banco de dados correspondente. Portanto, a mesma consulta executada com o adaptador PostgreSQL retornaria em vez disso
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)
O carregamento antecipado pode desencadear mais de uma consulta nos bastidores e algumas consultas
podem precisar dos resultados das consultas anteriores. Por causa disso, explain
realmente
executa a consulta e, em seguida, solicita os planos de consulta. Por exemplo,
ruby
Customer.where(id: 1).includes(:orders).explain
pode resultar nisso para MySQL e 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 row in set (0.00 sec)
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 row in set (0.00 sec)
e pode resultar nisso 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 rows)
21.1 Opções de Explicação
Para bancos de dados e adaptadores que suportam (atualmente PostgreSQL e MySQL), opções podem ser passadas para fornecer uma análise mais profunda.
Usando PostgreSQL, o seguinte:
Customer.where(id: 1).joins(:orders).explain(:analyze, :verbose)
resulta em:
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 rows)
Usando MySQL ou MariaDB, o seguinte:
Customer.where(id: 1).joins(:orders).explain(:analyze)
resulta em:
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 row in set (0.00 sec)
NOTA: As opções EXPLAIN e ANALYZE variam de acordo com as versões do MySQL e MariaDB. (MySQL 5.7, MySQL 8.0, MariaDB)
21.2 Interpretação do EXPLAIN
A interpretação da saída do EXPLAIN está além do escopo deste guia. As seguintes dicas podem ser úteis:
SQLite3: EXPLAIN QUERY PLAN
MySQL: EXPLAIN Output Format
MariaDB: EXPLAIN
PostgreSQL: Using EXPLAIN
Feedback
Você é incentivado a ajudar a melhorar a qualidade deste guia.
Por favor, contribua se encontrar algum erro de digitação ou factual. Para começar, você pode ler nossa contribuição à documentação seção.
Você também pode encontrar conteúdo incompleto ou desatualizado. Por favor, adicione qualquer documentação ausente para o principal. Certifique-se de verificar Guias Edge primeiro para verificar se os problemas já foram corrigidos ou não no branch principal. Verifique as Diretrizes dos Guias do Ruby on Rails para estilo e convenções.
Se por algum motivo você encontrar algo para corrigir, mas não puder corrigi-lo você mesmo, por favor abra uma issue.
E por último, mas não menos importante, qualquer tipo de discussão sobre a documentação do Ruby on Rails é muito bem-vinda no Fórum oficial do Ruby on Rails.