Tips and Tricks Ruby on Rails
Inspirações e Conteúdo
Este conteúdo foi inspirado por:
- Drifting Ruby - Screencasts de Ruby on Rails
- Videos de TI - Curso com conteúdo em português do @jacksonpires
- thoughtbot - Cursos com conteúdo em inglês
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?
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
- Truques e Navegação no Console
- Fundamentos do Active Record
- Exemplos Práticos no Console
- Conclusão
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
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
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
🛠️ 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"
🔄 Reload
# Recarregar código sem sair do console
reload!
🔍 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)
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')
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!
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
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
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
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)
✅ 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)
📊 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!)
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)
🔗 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))
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
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)
Ótimo o artigo, parabéns!!! Gostaria de acrescentar o uso do .none? que também evita carregar registros.