- Регистрация
- 9 Май 2015
- Сообщения
- 1,483
- Баллы
- 155

Este post foi 100% criado com meus exemplos, código e experiências reais, mas formatado com ajuda de AI para melhor organização. A AI pode nos ajudar a formatar e estruturar conteúdo, mas não substitui o conhecimento e experiência prática que nós, desenvolvedores, trazemos!

Importar dados de arquivos JSON ou Excel é uma tarefa comum no dia a dia de desenvolvimento Rails, mas muita gente ainda faz isso de forma ineficiente. Hoje vou mostrar três abordagens com resultados impressionantes: em um teste local com apenas 10.000 registros, a diferença foi de ~40 segundos para ~5 segundos - uma melhoria de mais de 8x!
O que este post NÃO aborda

- Como ler arquivos muito grandes de forma eficiente (isso fica para outro post)
- Importações por chunks/batches avançados (para não deixar o post muito extenso)
- Estratégias de paralelização com Sidekiq/ActiveJob
Preparando Nosso CenárioDica de ouro: Para quem usa PostgreSQL, o livro é leitura obrigatória!

Primeiro, vamos criar dados mockados para testar nossas implementações:
# file_generator.rb (na raiz do projeto)
require 'json'
require 'faker'
base_users = [
{ name: "John Smith", email: "john@example.com", bio: "Ruby on Rails dev..." },
{ name: "Sarah Johnson", email: "sarah@example.com", bio: "Tech Lead..." },
{ name: "Mike Wilson", email: "mike@example.com", bio: "Full-stack dev..." },
{ name: "Emma Davis", email: "emma@example.com", bio: "DevOps engineer..." },
{ name: "Alex Rodriguez", email: "alex@example.com", bio: "Senior dev..." }
]
base_titles = [
"Complete Guide to Active Record Queries",
"Avoiding the N+1 Problem in Rails",
"TDD with RSpec: From Basic to Advanced"
]
base_contents = [
"In this article, we explore...",
"Let's dive deep into...",
"This tutorial covers..."
]
base_categories = ["Ruby on Rails", "Performance", "Database", "DevOps", "Architecture"]
posts = []
10_000.times do |i|
user = base_users.sample
post = {
title: "#{base_titles.sample} ##{i}",
content: "#{base_contents.sample} #{Faker::Lorem.paragraph(sentence_count: 5)}",
published: [true, false].sample,
user: user,
categories: base_categories.sample(rand(1..3))
}
posts << post
end
File.write("blog_json_data_10k.json", JSON.pretty_generate(posts))
Como executar

- Gere o arquivo JSON com dados de teste:
# Na raiz do seu projeto Rails, execute:
ruby file_generator.rb
Isso vai criar um arquivo blog_json_data_10k.json com 10.000 posts mockados.
- Para testar as diferentes implementações no Rails console:
# Abra o console Rails
rails console
# Para testar a implementação menos recomendada (prepare o café!)
Importer::BadImporter.import!
# Para testar a implementação razoável
Importer::BlogDataImporter.import!
# Para testar a implementação otimizada (vai voar!)
Importer::BlogDataImporterWithActiveRecordImport.import!
A Classe JsonImporter (Auxiliar)Nota: Você pode alterar 10_000.times para gerar mais ou menos dados. Para começar, 1.000 registros já mostram bem a diferença!

Você deve ter notado que estamos usando Importer::JsonImporter em todos os exemplos. É uma classe auxiliar simples para ler o arquivo JSON:
# app/services/importer/json_importer.rb
module Importer
class JsonImporter
attr_reader :file_name, :file_path
def initialize(file_name:)
@file_name = file_name
@file_path = Rails.root.join(file_name)
end
def import!
unless File.exist?(@file_path)
raise "File not found: #{@file_path}"
end
puts "

file_content = File.read(@file_path)
JSON.parse(file_content)
rescue JSON::ParserError => e
raise "Invalid JSON format: #{e.message}"
rescue StandardError => e
raise "Error reading file: #{e.message}"
end
end
end
Esta é uma implementação básica que carrega todo o arquivo na memória. Existem maneiras MUITO mais performáticas e eficientes de ler arquivos grandes (streaming, chunking, etc.), mas isso fica para outro post! O foco aqui é na importação dos dados para o banco.
Exemplo 1: O Jeito Menos Recomendado

# app/services/importer/bad_importer.rb
module Importer
class BadImporter
def self.import!
start_time = Time.current
blog_json = Importer::JsonImporter.new(file_name: "blog_json_data_10k.json").import!
# Processa cada post individualmente
blog_json.each do |post_data|
# Cria usuário para cada post (verifica duplicatas toda vez!)
user = User.find_or_create_by(email: post_data["user"]["email"]) do |u|
u.name = post_data["user"]["name"]
u.bio = post_data["user"]["bio"]
end
# Cria post
post = Post.find_or_create_by(title: post_data["title"]) do |p|
p.content = post_data["content"]
p.published = post_data["published"]
p.user = user
end
# Cria categorias para cada post (mais verificações!)
post_data["categories"].each do |category_name|
category = Category.find_or_create_by(name: category_name)
# Cria associação
PostCategory.find_or_create_by(post: post, category: category)
end
end
elapsed_time = Time.current - start_time
puts "Tempo total: #{elapsed_time.round(2)} segundos"
end
end
end
Por que esse código não é o mais recomendável?

- Query explosion: Para cada post, fazemos múltiplas queries (find_or_create_by)
- Sem transação: Se algo falhar no meio, você terá dados parciais no banco
- Performance sofrível: No meu teste, levou 38.43 segundos para apenas 10.000 registros!
- Uso desnecessário de recursos: Verifica duplicatas a cada iteração
- Sem proteção contra falhas: Um erro em qualquer linha pode deixar lixo no banco
Exemplo 2: O Jeito RazoávelRealidade: Já vi código assim em produção processando muitos de registros. Imagina o tempo!

# app/services/importer/blog_data_importer.rb
module Importer
class BlogDataImporter
def self.import!
start_time = Time.current
ActiveRecord::Base.transaction do
blog_json = Importer::JsonImporter.new(file_name: "blog_json_data_10k.json").import!
# Extrai dados únicos ANTES de inserir
categories = blog_json.map{ |post| post["categories"] }.flatten.uniq
users = blog_json.map{ |post| post["user"] }.uniq { |user| user["email"] }
# Insere categorias e usuários de uma vez só
Category.insert_all(categories.map { |category| {name: category} })
User.insert_all(users.map { |user| {name: user["name"], email: user["email"], bio: user["bio"]} })
# Cria hashes de lookup (OTIMIZAÇÃO CHAVE!)
categories_hash = Category.all.pluck(:name, :id).to_h
users_hash = User.all.pluck(:email, :id).to_h
# Importa posts
blog_json.map do |post|
result = {
title: post["title"],
content: post["content"],
published: post["published"],
user_id: users_hash[post["user"]["email"]]
}
post_data = Post.create!(result)
# Cria associações post-categoria em lote
PostCategory.insert_all(
post["categories"].map { |category|
{post_id: post_data.id, category_id: categories_hash[category]}
}
)
end
rescue ActiveRecord::RecordInvalid, StandardError => e
error = "Error: #{e.message}"
puts error
Rails.logger.error error
end
elapsed_time = Time.current - start_time
puts "Tempo total: #{elapsed_time.round(2)} segundos"
end
end
end
Por que este código é bem melhor?

- Usa transação: Garante atomicidade - ou importa tudo ou nada
- insert_all: Reduz drasticamente o número de queries
- Hashes de lookup: Transforma buscas O(n) em O(1) - genial!
- Processa duplicatas uma vez só: Muito mais eficiente

categories_hash = Category.all.pluck(:name, :id).to_h
# Resultado: {"Ruby on Rails" => 1, "Performance" => 2, ...}
Ao invés de buscar a categoria no banco para cada post (10.000 buscas!), fazemos uma query só e acessamos via hash. Isso é ouro puro para performance!
Exemplo 3: O Jeito OTIMIZADO com activerecord-import

Primeiro, adicione ao Gemfile:
gem 'activerecord-import'
# Se estiver usando PostgreSQL (como no exemplo do repositório)
gem 'pg', '~> 1.1'
Agora veja a mágica acontecer:Nota: Este post funciona com qualquer banco de dados (SQLite, MySQL, PostgreSQL). No código de exemplo que disponibilizei no GitHub, usei PostgreSQL, mas você pode usar o banco que preferir!
# app/services/importer/blog_data_importer_with_active_record_import.rb
module Importer
class BlogDataImporterWithActiveRecordImport
def self.import!
start_time = Time.current
ActiveRecord::Base.transaction do
blog_json = Importer::JsonImporter.new(file_name: "blog_json_data_10k.json").import!
# Prepara dados únicos
categories = blog_json.map{ |post| post["categories"] }.flatten.uniq
users = blog_json.map{ |post| post["user"] }.uniq { |user| user["email"] }
# Importa categorias e usuários em batch
category_objects = categories.map { |name| Category.new(name: name) }
Category.import category_objects, on_duplicate_key_ignore: true, validate: false
user_objects = users.map { |user|
User.new(name: user["name"], email: user["email"], bio: user["bio"])
}
User.import user_objects, on_duplicate_key_ignore: true, validate: false
# Cria hashes de lookup
categories_hash = Category.all.pluck(:name, :id).to_h
users_hash = User.all.pluck(:email, :id).to_h
# Importa posts em batches para economizar memória
blog_json.in_groups_of(1000, false) do |post_batch|
posts_to_import = post_batch.map do |post|
Post.new(
title: post["title"],
content: post["content"],
published: post["published"],
user_id: users_hash[post["user"]["email"]]
)
end
# Importa este batch de posts (sem validações para máxima performance!)
Post.import posts_to_import, validate: false
end
# Prepara e importa associações em batches
Post.where(title: blog_json.map { |p| p["title"] }).find_in_batches(batch_size: 1000) do |post_batch|
post_categories = []
post_batch.each do |post|
post_data = blog_json.find { |p| p["title"] == post.title }
post_data["categories"].each do |category_name|
post_categories << PostCategory.new(
post_id: post.id,
category_id: categories_hash[category_name]
)
end
end
# Importa as associações deste batch (sem validações!)
PostCategory.import post_categories, validate: false if post_categories.any?
end
rescue StandardError => e
error = "Error: #{e.message}"
puts error
Rails.logger.error error
end
elapsed_time = Time.current - start_time
puts "Tempo total: #{elapsed_time.round(2)} segundos"
end
end
end
Por que activerecord-import é SENSACIONAL?

- SQL otimizado: Gera um único INSERT com múltiplos VALUES
- Gestão de memória: Processa em batches de 1000 registros
- Flexibilidade total: Validações opcionais, upserts, callbacks
- Performance brutal: No meu teste: ~5 segundos!
A gem oferece opções poderosas:
- on_duplicate_key_ignore: Ignora duplicatas silenciosamente
- on_duplicate_key_update: Atualiza registros existentes
- validate: false: Pula validações do Rails (ganho extra de performance!)
- batch_size: Controla o uso de memória

Teste local com 10.000 registros em um Apple M3 Pro com 18GB RAM:
- BadImporter: ~40 segundos
- BlogDataImporter: ~15 segundos
- BlogDataImporterWithActiveRecordImport: ~5 segundos
A versão otimizada foi 8x mais rápida que a menos recomendada!
Com volumes maiores (100.000 ou 1.000.000 de registros), você precisaria de estratégias ainda mais avançadas como processamento paralelo, filas, ou ferramentas especializadas - mas mesmo essa versão já é infinitamente melhor que o approach tradicional!Importante sobre os tempos: Estes testes foram feitos em uma máquina potente (M3 Pro, 18GB RAM). Em uma VPS básica (1GB RAM, 1 vCPU), esses tempos podem ser 3-4x maiores - ou seja, o BadImporter poderia levar 2-3 minutos! Mas a proporção de melhoria se mantém: a versão otimizada sempre será muito mais rápida, independente do hardware.
Conclusão e Boas Práticas

A diferença entre uma importação mal feita e uma otimizada pode significar:
- Seu script rodando em segundos ao invés de horas
- Menos carga no banco de dados
- Menor uso de memória
- Rollback automático em caso de erro
Sempre use:
- Transações para garantir consistência
- Inserções em lote (insert_all ou activerecord-import)
- Hashes de lookup para evitar queries desnecessárias
- Processamento de dados únicos antes da inserção
- Batches para controlar uso de memória
activerecord-import é seu melhor amigo para importações em massa!
Gostou do post? Deixe um

Quer mais conteúdo sobre performance e banco de dados?
Comenta aí embaixo o que você gostaria de ver:
- Estratégias avançadas com a gem activerecord-import
- Migrations e alterações de colunas em bases de dados existentes com milhões de registros
- Casos reais com Kafka ou SQS para processamento assíncrono
- Outras estratégias de otimização de banco de dados
É só pedir que a gente prepara o conteúdo!

Источник: