Qual a diferença entre usar .each, .find_each, .find_in_batches e .in_batches no Rails?

Fala, gente!

Hoje vamos falar um pouco sobre os métodos .each, .find_each, .find_in_batches e o in_batches. O primeiro é o mais comum, mas os outros muita gente desconhece, então acho que vale comentar sobre eles. Vamos lá!

.each

Bom, o .each é o mais conhecido e você já deve ter usado em algum momento. Em resumo ele vai servir para iterar em uma coleção e você pode passar um bloco de código para ser executado para cada um dos elementos da coleção. Veja o exemplo:

arr = [1,2,3,4,5]
arr.each do |i|
 puts i * 2
end 

2
4
6
8
10

Como podemos observar para cada elemento do array imprimi o próprio elemento * 2. Até aqui nenhuma novidade.

.find_each

A primeira coisa que precisamos saber sobre o .find_each é que ele é um método do **ActiveRecord** (assim como os próximos dois métodos que veremos), ou seja, diferente do seu irmão .each ele só vai funcionar no Rails.

Sua funcionalidade também é iterar em uma coleção, no entanto a diferença é que enquanto o .each carrega todos os elementos da coleção em memória para só depois iterar, o .find_each vai carregar (inicialmente) 1000 elementos por vez, ou seja, são feitos lotes de 1000 elementos que são iterados um a um. Para nosso exemplo vamos começar criando uma app rails.

/> rails new testapp
/> cd testapp
/> rails g model Person name
/> rails db:create
/> rails db:migrate
/> rails console

Já dentro do rails console faça:

(1..10_000).each do |i|
  Person.create!(name: "person#{i}")
end

Prontinho, com isso temos uma app rails já com 10.000 pessoas cadastradas.

O exemplo básico seria rodar algo como :

Person.find_each do |person|
  puts person.name
end

Mas rodando o comando acima praticamente não veremos diferença entre o each e o find_each, no entanto, perceba a saída SQL no terminal informando que deve ser buscado apenas 1000 itens no banco de dados.

Person Load (0.2ms)  SELECT "people".* FROM "people" WHERE "people"."id" > ? ORDER BY "people"."id" ASC LIMIT ?  [["id", 10000], ["LIMIT", 1000]]

Daí temos a prova que só foram carregados 1000 elementos por vez. Mas a coisa pode ficar ainda melhor, podemos escolher carregar 5 elementos por vez ao invés de 1000, para isso rode:

Person.find_each(batch_size: 5) do |person|
  puts person.name
end

Assim teremos uma saída como essa abaixo, mostrando que o SQL busca agora de 5 em 5 registros.

  Person Load (0.1ms)  SELECT "people".* FROM "people" WHERE "people"."id" > ? ORDER BY "people"."id" ASC LIMIT ?  [["id", 9990], ["LIMIT", 5]]
person9991
person9992
person9993
person9994
person9995
  Person Load (0.1ms)  SELECT "people".* FROM "people" WHERE "people"."id" > ? ORDER BY "people"."id" ASC LIMIT ?  [["id", 9995], ["LIMIT", 5]]
person9996
person9997
person9998
person9999
person10000
  Person Load (0.1ms)  SELECT "people".* FROM "people" WHERE "people"."id" > ? ORDER BY "people"."id" ASC LIMIT ?  [["id", 10000], ["LIMIT", 5]]

Só com isso já temos uma bela vantagem sobre o .each pois imagine uma grande aplicação com milhões de registros a serem trabalhados, o .find_each vem pra resolver esse tipo de situação.

Além dessa opção de especificar a quantidade de items por lote, podemos usar algumas outras (:start, :finish e :error_on_ignore) que valem serem olhadas na documentação oficial.

.find_in_batches

Uma outra possibilidade é usar o .find_in_batches, que em resumo se parece muito como .find_each e sua diferença fica por conta de que ele vai carregar em memória o lote de elementos e você decide o que fazer com eles, com por exemplo, iterar nesse lote ou fazer qualquer outra coisa. Veja esse exemplo:

Person.find_in_batches(batch_size: 5) do |people|
  puts people.inspect
end
...
[#<Person id: 9996, name: "person9996", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 9997, name: "person9997", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 9998, name: "person9998", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 9999, name: "person9999", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 10000, name: "person10000", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">]

Perceba que o resultado é um array de Person, daí precisamos, por exemplo, iterar nesse array e fazer o que quiser. Veja:

Person.find_in_batches(batch_size: 5) do |people|
  people.each do |person|
    puts person.name
  end
end

...
  Person Load (0.1ms)  SELECT "people".* FROM "people" WHERE "people"."id" > ? ORDER BY "people"."id" ASC LIMIT ?  [["id", 9990], ["LIMIT", 5]]
person9991
person9992
person9993
person9994
person9995
  Person Load (0.1ms)  SELECT "people".* FROM "people" WHERE "people"."id" > ? ORDER BY "people"."id" ASC LIMIT ?  [["id", 9995], ["LIMIT", 5]]
person9996
person9997
person9998
person9999
person10000
  Person Load (0.1ms)  SELECT "people".* FROM "people" WHERE "people"."id" > ? ORDER BY "people"."id" ASC LIMIT ?  [["id", 10000], ["LIMIT", 5]]

As opções são praticamente as mesmas do find_each, mas claro que vale olhar a documentação para ver os exemplos.

in_batches

Por fim temos o .in_batches que é muito parecido com o.find_in_batches, no entanto ao invés de devolver um array de elementos, ele devolve um ActiveRecord::Relation, veja:

Person.in_batches(of: 5) do |relation|
  puts relation.inspect
end
...
#<ActiveRecord::Relation [#<Person id: 9996, name: "person9996", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 9997, name: "person9997", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 9998, name: "person9998", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 9999, name: "person9999", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">, #<Person id: 10000, name: "person10000", created_at: "2020-02-23 23:13:44", updated_at: "2020-02-23 23:13:44">]>

A grande magia aqui é que por retornar uma ActiveRecord::Relation você pode aplicar os métodos que já conhecemos como .delete_all, .update_all, dentre outros.

Aqui também vale ressaltar que as opções disponíveis para o uso com o .in_batches são diferentes (observe que usei o :of ao invés de :batch_size), então, como sempre, vale consultar a documentação e conhecer todos os detalhes.

Enfim, espero que tenha curtido mais essa curiosidade.

É isso! Até a próxima! 😉