Importaça de Dadas Em Rails: do Jeito Menos Indacou ao Otimizado 🚀🇧🇷
Este post foi 100% criado com meus exemplos, código e experimentadas reais, mas formatado com ajuda de ai para melanhor organização. Um podo de ai nos ajudar um formatar e estrutrar Conteúdo, mas não substitui o conhecimento e experimentam prática que nós, desenvolvedores, trazemos! 📦 código completo disponnvel no github: importação dados de arquivos json ou excel é uma tarefa comum no dia a dia de desonvolvimento trilhos, mas muita gente ainda faz isso de forma ineficácia. Hoje vou Mostar Três abordagena com os imensões: em um teste local com apenas 10.000 Registros, uma dif. 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 💡 Dica de ouro: Para quem USA PostgreSQL, o Livro de alto desempenho PostgreSQL for Rails É Leitura Obrigatária! Preparando Nosso Cenário 🎬 Primeiro, Vamos Criar Dados Mockados para testar Nossas implementações: # file_generator.rb (na raiz do projeto) reque ‘json’ reque ‘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_categorias = [“Ruby on Rails”, “Performance”, “Database”, “DevOps”, “Architecture”]
Postagens = []
10_000.Times Do | i | user = base_users.sample post = {title: “#{base_titles.sample} ## {i}”, content: “#{base_contents.sample}#{faker :: lorem.paragraph (sentença_count: 5)}, publicado:: [true, false].Sample, Usuário: Usuário, Categorias: Base_categories.sample (Rand (1..3))} Postagens< post
end
File.write(“blog_json_data_10k.json”, JSON.pretty_generate(posts))
Enter fullscreen mode
Exit fullscreen mode
Como executar 🏃♂️
Gere o arquivo JSON com dados de teste:
# Na raiz do seu projeto Rails, execute:
ruby file_generator.rb
Enter fullscreen mode
Exit fullscreen mode
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!
Enter fullscreen mode
Exit fullscreen mode
💡 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!
A Classe JsonImporter (Auxiliar) 📄
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 “📁 Reading JSON file: #{@file_name}”
file_content = File.read(@file_path)
JSON.parse(file_content)
rescue JSON::ParserError => e aumentam “formato inválido json: #{e.message}” resgate standarderror => e aumentando “erro de leitura de erro: #{e.message}” final final End Enter Modo de tela completa Sair do modo de tela completa Esta Ó uma implementação de um básica que carrega para o arquivo na memora. Existem Maneiras Muito mais perforticas eficientes de ler Arquivos Grandes (Streaming, Chunking, etc.), mas isso fica para post! O foco aqui é na importação dos dados para o Banco. Exemplo 1: o jeito menos recomendado ❌ # app/serviços/importador/bad_importer.rb Module Industor Classe BadImter def self.import! start_time = time.current blog_json = importador :: jsonimporter.new (file_name: “blog_json_data_10k.json”). importar! # 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”]) faça | u | ONUE = POST_DATA[“user”][“name”]
U.bio = post_data[“user”][“bio”]
final # cria post post = post.find_or_create_by (título: post_data[“title”]) do | p | P.Content = post_data[“content”]
P.bublished = Post_Data[“published”]
P.User = Usuário final # CRIA Categorias para Cada Post (Mais Verificações!) Post_data[“categories”].ECH do | category_name | category = category.find_or_create_by (nome: category_name) # cria associação postcategory.find_or_create_by (postagem: post, categoria: categoria) final EXTAPSED_TIMED = Time.Current – Start_time Puts “TEMPO TOTAL: # {ENDENDEND.RUNDEN (2). Que esse código não é o mais RECOMENDável? 🤔 Explosão de consultas: Para Cada Post, Fazemos Múltiplas Querias (find_or_create_by) sem transmissão: SE Algo Falhar No Meio, Você Terá Dadas Parciais No Banco Performance Sofrí: No Meu Teste, Levou 38.43 Segundos USO DESNECESSÁRIO DE RECURSOS: Verifica Duplicatas a Cada IteraiaCão Sem Proteção Contra Falhas: UM Erro Em Qualquer Linha Linha Deixar Lixo No Banco ⚠️ Realidade: Já Vi Código Assim Em Produno Processando Mosco Muitos de Registros. Imagina O Tempo! Exemplo 2: o Jeito Razoável ✅ # App/Services/Importador/Blog_data_importer.rb Module Importador BlogDataImporter Def Self.import! start_time = time.current ActiveRecord :: base.Transaction do blog_json = importador :: jsonimporter.new (file_name: “blog_json_data_10k.json”). importar! # Extrai Dados Únicos antes de inserir categorias = blog_json.map {| post | publicar[“categories”] } .flatten.uniq usuários = blog_json.map {| post | publicar[“user”] } .uniq {| usuário | usuário[“email”] } # Insere categorias e usuários de Uma vez só category.insert_all (categories.map {| category | {name: category}}) user.insert_all (users.map {| user | {nome: usuário: usuário[“name”]Email: Usuário[“email”]Biografia: Usuário[“bio”]}}) # Cria hashes de lookup (otimizaça 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 | resultado = {title: post[“title”]Conteúdo: Post[“content”]Publicado: Post[“published”]user_id: usuários_hash[post[“user”][“email”]]} post_data = post.create! (Resultado) # Cria associações pós-categoria em lote postcategory.insert_all (post[“categories”].map {| categoria | {post_id: post_data.id, category_id: categories_hash[category]}}) resgate final ActiveRecord :: RecordinValid, Standarderror => e Error = “Erro: #{E.Message}” coloca trilhos de erro.Logger.error Error End Dropsed_time = time.Current – Start_time Puts “TEMPO Total: #{elapsed_time.round (ENDENDON) código é bem Melhor? 👍 EUA Transacão: Garante Atomicidade – Ou a importação tudo ou nada insert_all: Reduz Drasticementamento de Número de Consultas Hashes de Lookup: TransformA Buscas o (n) em o (1) – Genial! Processa duplicatas um vez só: muito mais eficientte a mágica dos hashes de lookup 🎩 categorias_hash = category.all.pluck (: name ,: id) .to_h # resultado: {“ruby em trilhos” => 1, “performance a”> 2, …} Enter Mode completa CADA POST (10.000 BUSCAS!), Fazemos uma consulta Só e acessamos via hash. ISSO É OURO PURO PARA ALEMPERAÇÃO! Exemplo 3: O Jeito Otimizado com ActiveRecord-Import 🚀 Primeiro, Adicione ao GemFile: Gem ‘ActiveRecord-Import’ # se Estiver USANando PostGresql (sem full-stro-Sell-Sell-DO Repositór) pg ‘,’ ~> 1.1 ‘enterro completo Modo Banco de Dadas (Sqlite, MySQL, PostgreSQL). Sem cáldigo de exemplos que descartizei no github, usei pósgresql, mas você pode usar o banco que preferir! AGORA VEJA A MÁGICA ACONTECER: # APP/Services/Importor/Blog_Data_Iporter_With_Active_Record_Import.rb Module Importador BlogDataimporterWithactiveReCordimport Def Self.import! start_time = time.current ActiveRecord :: base.Transaction do blog_json = importador :: jsonimporter.new (file_name: “blog_json_data_10k.json”). importar! # Prepara Dados Únicos categorias = blog_json.map {| post | publicar[“categories”] } .flatten.uniq usuários = blog_json.map {| post | publicar[“user”] } .uniq {| usuário | usuário[“email”] } # Categorias de importação e Usuários em lote category_objects = categories.map {| nome | Category.new (nome: nome)} category.import category_objects, on_duplicate_key_ignore: true, validate: false user_objects = users.map {| user | User.New (Nome: Usuário[“name”]Email: Usuário[“email”]Biografia: Usuário[“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) .oh # # # # false) do | post_batch | POST_TO_IMPORT = post_batch.map do | post | Post.New (Título: Post[“title”]Conteúdo: Post[“content”]Publicado: Post[“published”]user_id: usuários_hash[post[“user”][“email”]]) Fim # Importa Este lote de postagens (válvulas SEM Validações para Máxima Performance!) POST.IMPORT POST_TO_IMPORT, VALIDAÇÃO: FALSE END # prepara e importar associações em lotes post.where (título: 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”].ECH do | category_name | post_categorias< 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 = “Erro: #{e.message}” coloca trilhos de erro.logger.error error End EXTAPSED_TIME = time.current – start_time coloca “Tempo Total: #{elepSed_time.Round (2)} Segundos” end end Enter Fullscreen Exit Exitlel Mode? 🎯 Sql otimizado: gera um Único inserir com múltiplos valores gestão de memórórica: processos em lotes de 1000 registros flexibilidade Total: Validaições Opcionais, UpSerts, retorno de chamada Brutal: No Meu Teste: ~ 5 seguundos! A Gem OFERECE OPÇÕES PODEROSAS: ON_DUPLICATE_KEY_IGNORE: IGNORA DUPLICATAS SILENCIOSAMENTE ON_DUPLICATE_KEY_UPDATE: ATUALIZA REGISTROS EXISTES VALIDA: FALSO: LINCELAÇÃO DE ROLOS O MEMERIA DO MEMAFERA!) com 10.000 Registros Em UM Apple M3 Pro com 18 GB RAM: BadImter: ~ 40 Segundos 😱 BlogDataImporter: ~ 15 Segundos 😊 BlogDataImporterWithactiveRecordimport: ~ 5 segunda 🚀 a versão otimizada foi 8x mais rápida que um menos ⚠️ IMPORTANTE SOBRE OS TEMPOS: ESTES Testes Foram Feitos Em Uma Máquina Potente (M3 Pro, 18 GB de RAM). Em um VPS Básica (1 GB de RAM, 1 VCPU), Esses tempos Podem Ser 3-4x Maiores-Ou Seja, Ó Badimporter Poderia Levar 2-3 Minutos! Mas a proporção de meushoria se mantém: um versão otimizada semper será muito mais rápida, independente do hardware. Com volume de maiores (100.000 ou 1.000.000 de registros), você precisaria de estratégias ainda mais avançadas como processamento paralelo, filas, ou feramentos especializadas – mas meso essA esma já »infinitamento melhorize o abordagem o abordagem de freio o ourno! Conclusão e Boas Prático 🎉 uma diferença Entre Uma Importação Mal Feita E Uma Otimizada Pode Significar: Seu Script Rodando em Seguundos Ao Invés de Horas Menos Carga No Banco de Dados Menor Uso De MemoRaRaRaRaRaRaRaRaRaRaTaRaTaMaMa -Menos Caso No Caso DeSoMe Menor Uso de Memóia Rollback Rollback Inserções em lote (insert_all ou Activerecord-import) hashes de lookup para evitar perguntas desnecessárias processamento de dadas Únnicos antes da inserção lotes para controlar uso de memórórica activerecord-imless »seu melhor amigo para importa Gostou faz postagem? Deixe um ❤️ E Compartilhe com os Rails de Devs dos Ultos! Querida performance e banco de Dadas? Comenta aí embaido o que você gostaria de ver: Estratégias avanquada com uma gema migrações de gemecord-import e alterações de colunas em bases de DADOS existentes com milhões de registros casais com coofka ou sqs paratonamento assí-losncrões sobre os casais com os sqs de qs, o processo de assí-losnrsnCrões sobre os casais com os sqs paraostimesnncronnconncurs casais com o outono De Dadas é Só Pedir que um Gente Prepara O Conteúdo! 🚀
Fonte
Publicar comentário