Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷

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, dese…


This content originally appeared on DEV Community and was authored by Rodrigo Barreto

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!

📦 Código completo disponível no GitHub: https://github.com/rodrigonbarreto/event_reservation_system/tree/blog_post_import_data

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

💡 Dica de ouro: Para quem usa PostgreSQL, o livro High Performance 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)
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 🏃‍♂️

  1. 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.

  1. 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!

💡 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
      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? 🤔

  1. Query explosion: Para cada post, fazemos múltiplas queries (find_or_create_by)
  2. Sem transação: Se algo falhar no meio, você terá dados parciais no banco
  3. Performance sofrível: No meu teste, levou 38.43 segundos para apenas 10.000 registros!
  4. Uso desnecessário de recursos: Verifica duplicatas a cada iteração
  5. Sem proteção contra falhas: Um erro em qualquer linha pode deixar lixo no banco

⚠️ Realidade: Já vi código assim em produção processando muitos de registros. Imagina o tempo!

Exemplo 2: O Jeito Razoável ✅

# 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? 👍

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

A Mágica dos Hashes de Lookup 🎩

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'

💡 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!

Agora veja a mágica acontecer:

# 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? 🎯

  1. SQL otimizado: Gera um único INSERT com múltiplos VALUES
  2. Gestão de memória: Processa em batches de 1000 registros
  3. Flexibilidade total: Validações opcionais, upserts, callbacks
  4. 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

Comparação de Performance Real 📊

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!

⚠️ 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.

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!

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:

  1. Transações para garantir consistência
  2. Inserções em lote (insert_all ou activerecord-import)
  3. Hashes de lookup para evitar queries desnecessárias
  4. Processamento de dados únicos antes da inserção
  5. Batches para controlar uso de memória

activerecord-import é seu melhor amigo para importações em massa!

Gostou do post? Deixe um ❤️ e compartilhe com outros devs Rails!

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! 🚀

rails #ruby #performance #database #postgresql #activerecord


This content originally appeared on DEV Community and was authored by Rodrigo Barreto


Print Share Comment Cite Upload Translate Updates
APA

Rodrigo Barreto | Sciencx (2025-08-23T23:11:05+00:00) Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷. Retrieved from https://www.scien.cx/2025/08/23/importacao-de-dados-em-rails-do-jeito-menos-indicado-ao-otimizado-%f0%9f%9a%80%f0%9f%87%a7%f0%9f%87%b7/

MLA
" » Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷." Rodrigo Barreto | Sciencx - Saturday August 23, 2025, https://www.scien.cx/2025/08/23/importacao-de-dados-em-rails-do-jeito-menos-indicado-ao-otimizado-%f0%9f%9a%80%f0%9f%87%a7%f0%9f%87%b7/
HARVARD
Rodrigo Barreto | Sciencx Saturday August 23, 2025 » Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷., viewed ,<https://www.scien.cx/2025/08/23/importacao-de-dados-em-rails-do-jeito-menos-indicado-ao-otimizado-%f0%9f%9a%80%f0%9f%87%a7%f0%9f%87%b7/>
VANCOUVER
Rodrigo Barreto | Sciencx - » Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/23/importacao-de-dados-em-rails-do-jeito-menos-indicado-ao-otimizado-%f0%9f%9a%80%f0%9f%87%a7%f0%9f%87%b7/
CHICAGO
" » Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷." Rodrigo Barreto | Sciencx - Accessed . https://www.scien.cx/2025/08/23/importacao-de-dados-em-rails-do-jeito-menos-indicado-ao-otimizado-%f0%9f%9a%80%f0%9f%87%a7%f0%9f%87%b7/
IEEE
" » Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷." Rodrigo Barreto | Sciencx [Online]. Available: https://www.scien.cx/2025/08/23/importacao-de-dados-em-rails-do-jeito-menos-indicado-ao-otimizado-%f0%9f%9a%80%f0%9f%87%a7%f0%9f%87%b7/. [Accessed: ]
rf:citation
» Importação de Dados em Rails: Do Jeito Menos Indicado ao Otimizado 🚀🇧🇷 | Rodrigo Barreto | Sciencx | https://www.scien.cx/2025/08/23/importacao-de-dados-em-rails-do-jeito-menos-indicado-ao-otimizado-%f0%9f%9a%80%f0%9f%87%a7%f0%9f%87%b7/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.