O Rails 2.3 trouxe uma nova funcionalidade bastante interessante para quem trabalha com uma grande quantidade de informações e precisa efetuar rotinas de processamento em todos os registros (ou em um subset deles) de uma única vez: processamento em lote.

O funcionamento é simplificado, são dois métodos que fazem todo o trabalho:

  • find_in_batches: retorna grupos de 1000 registros (por padrão) para serem processados;
  • find_each: se utiliza do find_in_batches, carregando os registros em lote da mesma maneira, porém retornando cada registro para ser processado.

Veremos como isso funciona na prática. Para os exemplos abaixo considere que temos o seguinte modelo:

class Person < ActiveRecord::Base
  named_scope :all_active, :conditions => { :active => true }

  def process_something
    # Aqui podemos criar nosso método para efetuar o processamento desejado.
  end
end

Considere também que temos em torno de 10.000 registros de pessoas no banco de dados. Mais abaixo veremos o porquê.

find_in_batches

# Processando todas as pessoas ativas no banco de dados
Person.find_in_batches(:conditions => { :active => true }) do |people_batch|
  people_batch.each { |person| person.process_something  }
end

Aqui estamos processando somente as pessoas ativas, em grupos de 1.000, conforme já falamos.

O método find_in_batches funciona da seguinte maneira: ele carrega do banco de dados todos os registros que tenham o id maior ou igual a 0 (por padrão), com um limite máximo de 1.000, ou seja, os primeiros 1.000 registros, e retorna esse lote. Após ser processado, ele busca os próximos 1.000, contando a partir do id do último registro processado anteriormente, e assim sucessivamente até não haver mais registros.

Para que o método funcione corretamente é necessário que a chave primária da tabela, no caso do padrão do Rails o campo id, seja do tipo inteiro. Isto é obrigatório pois o find_in_batches usa o valor da chave para contar os registros a cada lote. Ele também sempre ordena pela chave primária em ordem crescente, para se utilizar do índice da chave e otimizar a performance, significando que não podemos definir a ordenação. Outro parâmetro que não pode ser definido é o limite de registros, pois ele é usado internamente pelo método para controlar o tamanho dos lotes.

Todas as demais opções que são utilizadas no método find comum funcionam também para o find_in_batches, como as condições passadas no exemplo. Também é possível utilizar o método com named_scopes:

Person.all_active.find_in_batches(:batch_size => 500) do |people_batch|
  people_batch.each { |person| person.process_something  }
end

Notem também que foi passado um parâmetro diferente: batch_size. Este parâmetro identifica o número de registros por lote, e é usado internamente pelo método para montar a cláusula limit.

Ainda é possível definir uma opção para iniciar a contagem de registros: start. Essa opção define a partir de qual registro deve ser retornado, por exemplo, com o parâmetro :start => 50, o método irá retornar todos os registros a partir do id 50.

find_each

O método find_each funciona como um wrapper sobre o find_in_batches, retornando cada registro retornado pelos lotes. Vamos ver o mesmo exemplo que utilizamos anteriormente, agora com o find_each:

Person.find_each(:conditions => { :active => true }) do |person|
  person.process_something
end

Da mesma forma que o find_in_batches, este método funciona com named_scopes:

Person.all_active.find_each do |person|
  person.process_something
end

Finalizando

Os métodos para processamento em lote são bastante úteis para efetuar rotinas em grandes números de registros, contudo não devem ser utilizados quando você está trabalhando com poucos registros. O objetivo é carregar da forma mais rápida possível todos os registros, mas em grupos, para não carregar todos de uma vez em memória. Por isso quando se está trabalhando com poucos registros não há porque utilizar essa funcionalidade. Neste caso, pode-se utilizar o find comum.