Importando dados com rails na velocidade da luz
Caio Ramos
14/09/2020

Bom se você chegou até aqui acredito que você também já teve dor de cabeça ao importar dados para seu projeto, seja em desenvolvimento ou em produção (coragem hein, rs) não é uma tarefa das mais fáceis, tão pouco rapidas.
Imagine o seguinte cenário, é requesitado que você faça a inserção de 830 mil linhas de registros em um bd local, a fim de desenvolvimento e posterior implementações, nada demais certo, esses dados foram obtidos de outros setores e atualmente está em um arquivo .csv.
Existem inúmeras formas de concluir a tarefa, mas no geral você itera nas linhas, resgata os dados e faz a inserção no banco, procedimento de rotina certo?
Para motivos de introdução do conceito irei demonstrar dois possíveis caminhos para resolução da tarefa:
No primeiro iremos a cada linha inserir no banco o registro, algo assim:
# Supondo que o modelo seja User com os campos name e email
require 'csv'
file_path = 'caminho/para/seu/arquivo.csv'
CSV.foreach(file_path, headers: true) do |row|
User.create(name: row['name'], email: row['email'])
end
É um código simples e funcional, não hà nada de errado com ele, pode apostar que ele irá funcionar. Mas se vai rodar rápido? Com certeza não!
Com o citado arquivo csv anterior a task, rodando localmente, demorou cerca de oito horas e meia. E não, não é nem uma tabela com milhares de colunas com capacidade para milhares de carácteres estamos falando apenas de cinco colunas de texto puro que não passam de vinte carácteres, loucura não?
Para o segundo caso mudaremos muito pouco, veja abaixo:
file_path = 'caminho/para/seu/arquivo.csv'
users_data = []
CSV.foreach(file_path, headers: true) do |row|
users_data << {name: row['name'], email: row['email']}
end
User.insert_all(users_data)
Agora ao invés de linha a linha ler e inserir no banco, leremos o arquivo csv inteiro, armazenaremos-o em uma váriavel para depois inserir todos os dados de uma única vez usando o comando insert_all que por acaso é nativo do rails (link). Somente esssa alteração proporcionou diminuir o tempo de inserção das oito horas e meia anterior para apenas um minuto e vinte e seis segundos!
Mas Caio, como isso é possível? :O
Bulk insert
É um processo ou método fornecido por um sistema de gerenciamento de banco de dados para carregar várias linhas de dados em uma tabela de banco de dados em uma única vez. Este mecanismo tem algumas vantagens em relação a muitas inserções únicas. A principal vantagem é a performance do insert com relação a tempo.
Transformando seus dados em hashes ou arrays todos antes de inserir faz com que o uso da memória RAM seja melhor controlado, ou seja, trabalhando de uma forma inteligente você consegue diminuir a quantidade de RAM requerida para a mesma tarefa.
No entanto, existem algumas desvantagens com o uso de tal mecanismo. Por exemplo, você pode ter modelos ActiveRecord com alguns callbacks, esses provavelmente falhariam. Isso acontece porque você não pode garantir que todos os recursos necessários estejam na memória. Por exemplo, se você tem um campo requerido descrito no schema, mas no seu arquivo não consta aquela informação, você receberá apenas um erro, não tem choro nem vela.
Como disse no inicio do post existe milhares de maneiras de se inserir dados no banco, essa são apenas duas delas. Eaí o que achou, oito horas e meia para um minuto e vinte seis segundos é uma grande diferença, não?
Bônus
Sobre as callbacks que podem quebrar seu sonho de importar rápido como a velocidade da luz, tenho outra dica:
ActiveRecord Import
ActiveRecord Import é uma rails gem que ajuda você a fazer inserções em massa usando ActiveRecord. Claro, isso não parece tão emocionante, mas na verdade é.
Se você tiver que inserir milhares de registros usando ActiveRecord, como vimos antes, se não feito de uma forma bem pensada, pode ser muito lento, se você tiver callbacks e seu arquivo de origem não for tão confiável a tarefa por virar um pesadelo.
Isso está longe de ser ótimo …
Em casos como esse esta gema diminuirá suas inserções de milhares para um, assim como o insert_all, mas com essa gema podemos trabalhar e ou contornar as callbacks da model. Aqui está uma lista dos excelentes recursos que esta joia possui:
Usa objetos ActiveRecord ou matrizes de colunas e valores para importar dados
Usa uma opção recursiva para lidar com modelos relacionados incorporados (somente PostgreSQL)
Pula ou aplica as validações do modelo
Só pode importar um conjunto de atributos do modelo
Divide sua importação em lotes, ou seja, definindo o número de linhas por inserção
Lida com duplicatas, ignorando-as ou definindo quais atributos podem ser atualizados
Exemplo de uso:
require 'csv'
require 'activerecord-import'
file_path = 'caminho/para/seu/arquivo.csv'
users_data = []
CSV.foreach(file_path, headers: true) do |row|
users_data << User.new(name: row['name'], email: row['email'])
end
# Inserção em lote com validações
results = User.import users_data, validate: true
if results.failed_instances.empty?
puts "Todos os registros foram importados com sucesso."
else
puts "Alguns registros falharam a validação e não foram importados."
end
