DEV Community

Rodrigo Barreto for Vídeos de Ti

Posted on

Dicas e truques Ruby on Rails

Tips and Tricks Ruby on Rails

Inspirações e Conteúdo

Este conteúdo foi inspirado por:

Observação: Não deixe a AI fazer o seu serviço, mas deixe ela te ajudar! Todo o conteúdo e experiência são meus, mas foi formatado e organizado com ajuda de AI para melhor clareza e estrutura.


Outro dia um amigo me pediu uma ajuda no projeto dele, e perdemos horas dando boas risadas para matar a saudade e ele acabou pegando várias dicas comigo no Ruby on Rails, coisas simples, mas que podem ajudar um Junior e um Medior.

No final da conversa ele até comentou - no caso ele me chama de xará pois o nome dele também é Rodrigo - "porque você não cria um post sobre isso???"

E eu falei: "olha, porque não?"

O título "Tips and Tricks" veio do Drifting Ruby episódio com o mesmo nome. Lá tem muitos outros exemplos, mas literalmente essa única parte eu trouxe de lá:

Project.find(1).present? 
Project.exists?(id: 1) 
Project.where(id: 1).any?
Enter fullscreen mode Exit fullscreen mode

Por sinal, recomendo a assinatura - eles têm muitas coisas legais.

Além disso, se você quiser aprender melhor Ruby on Rails, recomendo o Videos de TI (só tem em português), mas eu recomendo - é do meu amigo @jacksonpires, ele é um ótimo professor.

Já a thoughtbot tem ótimos cursos em inglês.

Bora lá, vamos montar esse post!

Índice

Setup do Projeto

Primeiro, vamos criar os modelos de um blog simples para testar nossos exemplos:

📁 Projeto completo disponível em: github.com/rodrigonbarreto/event_reservation_system

Comandos Rails Generate

# Gerar os modelos
rails generate model User name:string email:string bio:text
rails generate model Category name:string description: "text"
rails generate model Post title: "string content:text published:boolean user:references"
rails generate model Comment content:text post:references user:references
rails generate model PostCategory post:references category:references

rails db:migrate
rails db:seed
Enter fullscreen mode Exit fullscreen mode

Modelos Completos

📂 Clique para ver os modelos completos

# app/models/user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
  has_many :comments, dependent: :destroy
  has_many :post_categories, through: :posts
  has_many :categories, through: :post_categories

  validates :name, presence: true
  validates :email, presence: true, uniqueness: true

  def published_posts
    posts.where(published: true)
  end
end

# app/models/category.rb
class Category < ApplicationRecord
  has_many :post_categories, dependent: :destroy
  has_many :posts, through: :post_categories
  has_many :users, through: :posts

  validates :name, presence: true, uniqueness: true

  scope :with_published_posts, -> { joins(:posts).where(posts: { published: true }).distinct }

  def published_posts_count
    posts.where(published: true).count
  end
end

# app/models/post.rb
class Post < ApplicationRecord
  belongs_to :user
  has_many :comments, dependent: :destroy
  has_many :post_categories, dependent: :destroy
  has_many :categories, through: :post_categories

  validates :title, presence: true
  validates :content, presence: true

  scope :published, -> { where(published: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
  scope :by_user, ->(user) { where(user: user) }

  def comments_count
    comments.count
  end
end

# app/models/comment.rb
class Comment < ApplicationRecord
  belongs_to :post
  belongs_to :user

  validates :content, presence: true

  scope :recent, -> { where('created_at > ?', 1.day.ago) }
  scope :by_user, ->(user) { where(user: user) }

  delegate :title, to: :post, prefix: true
  delegate :name, to: :user, prefix: true
end

# app/models/post_category.rb
class PostCategory < ApplicationRecord
  belongs_to :post
  belongs_to :category

  validates :post_id, uniqueness: { scope: :category_id }

  scope :for_published_posts, -> { joins(:post).where(posts: { published: true }) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
end
Enter fullscreen mode Exit fullscreen mode

Truques e Navegação no Console

🎯 Magia do Underscore (_)

O underscore sempre retorna o resultado do último comando executado no console. É uma forma rápida de capturar o resultado anterior sem precisar executar a query novamente:

Comment.last
# => #<Comment id: 5, content: "Great post!", ...>

# O underscore (_) sempre vai conter o último resultado
comment = _
# => #<Comment id: 5, content: "Great post!", ...>

# Funciona com qualquer comando
User.where(name: "John").limit(3)
users = _  # Captura os 3 usuários sem executar a query novamente
Enter fullscreen mode Exit fullscreen mode

🛠️ Métodos Helper

# Ver helpers disponíveis
ActionController::Base.helpers

# Usar helpers do Rails
ActionController::Base.helpers.pluralize(5, 'post')
# => "5 posts"

ActionController::Base.helpers.time_ago_in_words(1.hour.ago)
# => "about 1 hour"

ActionController::Base.helpers.number_to_currency(29.95)
# => "$29.95"
Enter fullscreen mode Exit fullscreen mode

🔄 Reload

# Recarregar código sem sair do console
reload!
Enter fullscreen mode Exit fullscreen mode

🔍 Truques de Inspeção

user = User.first

# Diferentes formatos de saída
user.attributes
# => {"id"=>1, "name"=>"John", ...}

user.attributes.to_json
# => "{\"id\":1,\"name\":\"John\",...}"

JSON.pretty_generate(user.attributes)
# => Saída JSON formatada e legível

# Pretty print
pp user.attributes

# Ver apenas keys
user.attributes.keys
# => ["id", "name", "email", "bio"]

# Inspecionar mudanças não salvas
user.name = "New Name"
user.changes
# => {"name"=>["John", "New Name"]} (mostra valor antigo -> valor novo)

user.changed_attributes
# => {"name"=>"John"} (apenas os valores antigos)

# Ver métodos disponíveis - útil para descobrir métodos que você não conhece
user.methods.sort                    # Todos os métodos em ordem alfabética
user.methods.grep(/name/)           # Apenas métodos que contém "name"
user.public_methods(false)          # Apenas métodos próprios da classe (sem herança)

# Inspect - Ver representação do objeto de forma legível
user.inspect
# => "#<User id: 1, name: \"John\", email: \"john@example.com\", bio: \"Developer...\">"

# Útil para debugging e logs
puts user.inspect
Rails.logger.info(user.inspect)
Enter fullscreen mode Exit fullscreen mode

Fundamentos do Active Record

⚡ Pluck vs Select - Performance na Recuperação de IDs

Quando você precisa apenas de IDs, existem duas abordagens principais:

# Exemplo: Buscar todos os posts da categoria "Ruby on Rails"

# ❌ Forma menos eficiente - carrega objetos completos
category_ids = Category.where(name: "Ruby on Rails").map(&:id)
posts = Post.joins(:post_categories).where(post_categories: { category_id: category_ids })

# ✅ PLUCK - Retorna array, executa 1 query + 1 query = 2 queries totais
ids = Category.where(name: "Ruby on Rails").pluck(:id)
# Category Pluck (0.2ms) SELECT "categories"."id" FROM "categories" WHERE "categories"."name" = 'Ruby on Rails'
posts = Post.joins(:post_categories).where(post_categories: { category_id: ids })
# Post Load (0.4ms) SELECT "posts".* FROM "posts" INNER JOIN "post_categories"...

# ✅ SELECT - Retorna ActiveRecord::Relation, executa apenas 1 query otimizada
ids = Category.where(name: "Ruby on Rails").select(:id)
posts = Post.joins(:post_categories).where(post_categories: { category_id: ids })
# Post Load (0.4ms) SELECT "posts".* FROM "posts" INNER JOIN "post_categories" 
# WHERE "post_categories"."category_id" IN (SELECT "categories"."id" FROM "categories" WHERE "categories"."name" = 'Ruby on Rails')
Enter fullscreen mode Exit fullscreen mode

Diferenças importantes:

Método Retorno Queries executadas Use quando
pluck(:id) [1, 3, 5] (Array) 2 queries Precisar de array de valores
select(:id) ActiveRecord::Relation 1 query otimizada Usar em subqueries ou chains
# PLUCK - Executa a query imediatamente e retorna Array
ids = Category.where(name: "Ruby on Rails").pluck(:id)
# => [1, 3, 5] (Array de integers)

# SELECT - Retorna ActiveRecord::Relation, mais eficiente para chains
ids = Category.where(name: "Ruby on Rails").select(:id)
ids.class
# => Category::ActiveRecord_Relation

# Quando usado na próxima query, faz subquery otimizada em 1 comando só:
Post.where(category_id: ids)  # Uma query com subquery, não duas separadas!
Enter fullscreen mode Exit fullscreen mode

Na imagem abaixo, você pode ver a diferença prática:

  • PLUCK (2 queries): Quando executamos as duas linhas com pluck, o Rails faz duas queries separadas
  • SELECT (1 query): Quando executamos as duas linhas com select, o Rails otimiza e faz apenas uma query com subquery

🎯 Quando Usar user_id vs user.id

Uma das otimizações mais simples e importantes - quando você só precisa do ID, use o atributo direto:

comment = Comment.first
# Comment Load (0.3ms) SELECT "comments".* FROM "comments" ORDER BY "comments"."id" ASC LIMIT 1

# ✅ EFICIENTE - Usa o valor já carregado
comment.user_id
# => 5
# Sem query adicional!

# ❌ INEFICIENTE - Carrega o objeto User completo
comment.user.id  
# User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = 5 LIMIT 1
# => 5
Enter fullscreen mode Exit fullscreen mode

Exemplos práticos:

# ✅ EFICIENTE - Use quando precisar apenas do ID
if comment.user_id == current_user.id
  # Comparando apenas IDs, sem carregar user
end

# ❌ INEFICIENTE - Carrega o objeto User completo só para pegar o ID
if comment.user.id == current_user.id  
  # Isso carrega o objeto user inteiro desnecessariamente
end

# ✅ EFICIENTE - Use quando for usar outros atributos do user também
name = comment.user.name  # Agora estamos usando o user carregado
id = comment.user.id      # OK usar .id já que carregamos o user
Enter fullscreen mode Exit fullscreen mode

Impacto nos Testes (RSpec):

Em uma suite de testes, essa diferença pode ser significativa. Em 1000 testes, isso pode adicionar 2-3 minutos extras na execução:

# ❌ Em uma suite de 1000 testes, isso pode adicionar 2-3 MINUTOS
it "checks comment ownership" do
  expect(comment.user.id).to eq(current_user.id)  # Carrega user desnecessariamente
end

# ✅ Muito mais rápido - sem query extra
it "checks comment ownership" do
  expect(comment.user_id).to eq(current_user.id)  # Só usa o ID já carregado
end
Enter fullscreen mode Exit fullscreen mode

Quando usar cada um:

  • user_id: Para comparações, validações, foreign keys
  • user.id: Quando você já vai usar outros atributos do user depois

🚫 Problema N+1 & Includes

O problema N+1 é um dos mais críticos em performance:

# ❌ PROBLEMA N+1 - Executa 1 + N queries
posts = Post.all  # 1 query
posts.each do |post|
  puts post.user.name  # N queries (uma para cada post)
end

# ✅ SOLUÇÃO - Executa apenas 2 queries
posts = Post.includes(:user)  # 1 query + 1 query para users
posts.each do |post|
  puts post.user.name  # Sem queries adicionais
end

# Para relacionamentos mais complexos
Post.includes(:user, :comments, categories: :posts)
Enter fullscreen mode Exit fullscreen mode

✅ Verificações de Existência - Performance Importa

# ❌ INEFICIENTE - Carrega o objeto completo na memória
Project.find(1).present?  # SELECT * FROM projects WHERE id = 1

# ✅ EFICIENTE - Apenas verifica existência no banco
Project.exists?(id: 1)    # SELECT 1 FROM projects WHERE id = 1 LIMIT 1

# ✅ ALTERNATIVA - Para queries complexas
Project.where(id: 1, active: true).exists?

# ⚠️ CUIDADO - Carrega todos os registros
Project.where(id: 1).any?  # SELECT * (pode ser perigoso com muitos dados)
Enter fullscreen mode Exit fullscreen mode

📊 Count vs Size vs Length - Performance Crítica

# Cenário: User com muitos posts
user = User.first

# 1. COUNT - Sempre executa query SQL
user.posts.count
# => SELECT COUNT(*) FROM posts WHERE user_id = 1

# 2. SIZE - Inteligente: usa cache se collection já foi carregada
user.posts.size
# Se posts não foram carregados: SELECT COUNT(*)
# Se posts já foram carregados: posts.length (sem query)

# 3. LENGTH - Força carregamento completo da collection
user.posts.length
# => SELECT * FROM posts WHERE user_id = 1 (PERIGOSO!)
Enter fullscreen mode Exit fullscreen mode

Regra de ouro:

  • size: Use por padrão (99% dos casos)
  • count: Use quando precisar do valor exato do banco
  • length: Evite, exceto em collections pequenas já carregadas

🔍 Query Explain - Debug de Performance

# Analisar performance de queries
User.joins(:posts).where(posts: { published: true }).explain

# Resultado do IRB:
# User Load (1.2ms)  SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "posts"."published" = ?  [["published", 1]]
# => 
# EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" WHERE "posts"."published" = ? [["published", 1]]
# 3|0|0|SCAN posts
# 7|0|0|SEARCH users USING INTEGER PRIMARY KEY (rowid=?)

# Procure por:
# "SCAN" = Table scan completo (pode ser lento)
# "SEARCH USING INDEX" = Usando índice (rápido)  
# "USING INTEGER PRIMARY KEY" = Busca por chave primária (ótimo)
Enter fullscreen mode Exit fullscreen mode

🔗 Merge - Combinando Scopes com Elegância

O merge permite combinar condições de diferentes modelos de forma limpa:

# Cenário: Posts publicados de usuários ativos
class User < ApplicationRecord
  scope :active, -> { where(active: true) }
  scope :premium, -> { where(premium: true) }
end

class Post < ApplicationRecord
  scope :published, -> { where(published: true) }
  scope :recent, -> { where('created_at > ?', 1.week.ago) }
end

# ❌ Forma verbosa
Post.joins(:user)
    .where(published: true)
    .where(users: { active: true, premium: true })

# ✅ Forma elegante com merge + scopes
Post.published
    .joins(:user)
    .merge(User.active)
    .merge(User.premium)

# ✅ Merge também funciona com where direto
Post.joins(:user)
    .merge(User.where(active: true))
    .where(published: true)

# ✅ Combinando múltiplas condições
Post.joins(:user, :comments)
    .merge(User.where('created_at > ?', 1.year.ago))
    .merge(Comment.where('created_at > ?', 1.week.ago))
Enter fullscreen mode Exit fullscreen mode

Exemplos Práticos no Console

Agora que você já populou os dados com rails db:seed, teste estes comandos:

# Teste básico de relacionamentos
user = User.first
user.posts.published

# Teste de includes
Post.includes(:user, :categories, :comments).first

# Contagem por categoria
Category.joins(:posts).group('categories.name').count

# Exemplo de pluck vs select
Category.where(name: "Ruby on Rails").pluck(:id)
Category.where(name: "Ruby on Rails").select(:id)

# Teste de existence checks
Post.exists?(title: "Complete Guide to Active Record Queries")
Post.where(published: true).any?

# Performance comparison
user.posts.count
user.posts.size
Enter fullscreen mode Exit fullscreen mode

Conclusão

Essas dicas simples podem fazer uma grande diferença na performance e legibilidade do seu código Rails. Lembre-se:

  • ✅ Use pluck quando precisar apenas de valores específicos e não continuar com outras queries
  • ✅ Use select quando for usar em subqueries ou continuar o chain de queries
  • ✅ Use exists? em vez de carregar objetos só para verificar existência
  • ✅ Sempre prefira includes para evitar N+1
  • size é geralmente a melhor opção entre count/size/length
  • merge torna suas queries mais legíveis e reutilizáveis

💡 Dica: Se este conteúdo te ajudou, compartilhe com outros desenvolvedores!

Top comments (1)

Collapse
 
gregrio_neto_038e70732e5 profile image
Gregório Neto • Edited

Ótimo o artigo, parabéns!!! Gostaria de acrescentar o uso do .none? que também evita carregar registros.

# ✅ EFICIENTE - Semelhante ao `.exists?`, mas verifica se NÃO existem registros 
Project.where(active: true).none?  
# SELECT 1 FROM projects WHERE active = true LIMIT 1  
Enter fullscreen mode Exit fullscreen mode