Where This Came From
This content was inspired by:
- Drifting Ruby - Ruby on Rails screencasts
- Videos de TI - Portuguese course by @jacksonpires
- thoughtbot - English courses
Note: Don't let AI do your work, but let it help you! All the content and experience are mine, but it was formatted and organized with AI help for better clarity and structure.
The other day, a friend asked for help with his project. We spent hours catching up and laughing, and he ended up picking up several Rails tips from me - simple stuff that can help Junior and Mid-level developers.
At the end of our chat, he said - "Why don't you create a post about this???"
And I said: "you know what, why not?"
The "Tips and Tricks" title came from Drifting Ruby episode with the same name. They have lots of other examples there, but I literally brought just this one part from there:
Project.find(1).present?
Project.exists?(id: 1)
Project.where(id: 1).any?
By the way, I recommend the subscription - they have lots of cool stuff.
Also, if you want to learn Ruby on Rails better, I recommend Videos de TI (only in Portuguese), but I do recommend it - it's from my friend @jacksonpires, he's a great teacher.
And thoughtbot has awesome courses in English.
Let's go, let's build this post!
What's Inside
Project Setup
First, let's create some simple blog models to test our examples:
📁 Full project available at: github.com/rodrigonbarreto/event_reservation_system
Rails Generate Commands
# Create the models
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
Complete Models
📂 Click to see the complete models
# 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
Console Tips & Navigation
🎯 The Magic Underscore (_)
The underscore always returns the result of the last command you ran in the console. It's a quick way to grab the previous result without running the query again:
Comment.last
# => #<Comment id: 5, content: "Great post!", ...>
# The underscore (_) will always contain the last result
comment = _
# => #<Comment id: 5, content: "Great post!", ...>
# Works with any command
User.where(name: "John").limit(3)
users = _ # Grabs the 3 users without running the query again
🛠️ Helper Methods
# See available helpers
ActionController::Base.helpers
# Use Rails helpers
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
# Reload code without leaving the console
reload!
🔍 Inspection Tricks
user = User.first
# Different output formats
user.attributes
# => {"id"=>1, "name"=>"John", ...}
user.attributes.to_json
# => "{\"id\":1,\"name\":\"John\",...}"
JSON.pretty_generate(user.attributes)
# => Nice formatted JSON output
# Pretty print
pp user.attributes
# See just the keys
user.attributes.keys
# => ["id", "name", "email", "bio"]
# Check unsaved changes
user.name = "New Name"
user.changes
# => {"name"=>["John", "New Name"]} (shows old value -> new value)
user.changed_attributes
# => {"name"=>"John"} (just the old values)
# See available methods - great for finding methods you don't know about
user.methods.sort # All methods in alphabetical order
user.methods.grep(/name/) # Only methods that contain "name"
user.public_methods(false) # Only the class's own methods (no inheritance)
# Inspect - See a readable object representation
user.inspect
# => "#<User id: 1, name: \"John\", email: \"john@example.com\", bio: \"Developer...\">"
# Useful for debugging and logs
puts user.inspect
Rails.logger.info(user.inspect)
Active Record Basics
⚡ Pluck vs Select - Getting IDs Fast
When you only need IDs, there are two main approaches:
# Example: Get all posts from "Ruby on Rails" category
# ❌ Less efficient way - loads complete objects
category_ids = Category.where(name: "Ruby on Rails").map(&:id)
posts = Post.joins(:post_categories).where(post_categories: { category_id: category_ids })
# ✅ PLUCK - Returns array, runs 1 query + 1 query = 2 total queries
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 - Returns ActiveRecord::Relation, runs just 1 optimized query
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')
Key differences:
Method | Returns | Queries run | Use when |
---|---|---|---|
pluck(:id) |
[1, 3, 5] (Array) |
2 queries | Need array of values |
select(:id) |
ActiveRecord::Relation |
1 optimized query | Using in subqueries or chains |
# PLUCK - Runs the query right away and returns Array
ids = Category.where(name: "Ruby on Rails").pluck(:id)
# => [1, 3, 5] (Array of integers)
# SELECT - Returns ActiveRecord::Relation, better for chaining
ids = Category.where(name: "Ruby on Rails").select(:id)
ids.class
# => Category::ActiveRecord_Relation
# When used in the next query, makes an optimized subquery in 1 go:
Post.where(category_id: ids) # One query with subquery, not two separate ones!
In the image below, you can see the practical difference:
-
PLUCK (2 queries): When we run the two lines with
pluck
, Rails makes two separate queries -
SELECT (1 query): When we run the two lines with
select
, Rails optimizes and makes just one query with subquery
🎯 When to Use user_id vs user.id
One of the simplest and most important optimizations - when you only need the ID, use the direct attribute:
comment = Comment.first
# Comment Load (0.3ms) SELECT "comments".* FROM "comments" ORDER BY "comments"."id" ASC LIMIT 1
# ✅ EFFICIENT - Uses the already loaded value
comment.user_id
# => 5
# No extra query!
# ❌ INEFFICIENT - Loads the complete User object
comment.user.id
# User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = 5 LIMIT 1
# => 5
Practical examples:
# ✅ EFFICIENT - Use when you only need the ID
if comment.user_id == current_user.id
# Just comparing IDs, no need to load user
end
# ❌ INEFFICIENT - Loads the complete User object just for ID
if comment.user.id == current_user.id
# This loads the entire user object unnecessarily
end
# ✅ EFFICIENT - Use when you need other user attributes too
name = comment.user.name # Now we're using the loaded user
id = comment.user.id # OK to use .id since we already loaded user
Impact on Tests (RSpec):
In a test suite, this difference can be significant. In 1000 tests, this can add 2-3 extra minutes to execution:
# ❌ In a suite of 1000 tests, this can add 2-3 MINUTES
it "checks comment ownership" do
expect(comment.user.id).to eq(current_user.id) # Loads user unnecessarily
end
# ✅ Much faster - no extra query
it "checks comment ownership" do
expect(comment.user_id).to eq(current_user.id) # Just uses the already loaded ID
end
When to use each:
-
user_id
: For comparisons, validations, foreign keys -
user.id
: When you're going to use other user attributes later
🚫 N+1 Problem & Includes
The N+1 problem is one of the worst performance killers:
# ❌ N+1 PROBLEM - Runs 1 + N queries
posts = Post.all # 1 query
posts.each do |post|
puts post.user.name # N queries (one for each post)
end
# ✅ SOLUTION - Runs just 2 queries
posts = Post.includes(:user) # 1 query + 1 query for users
posts.each do |post|
puts post.user.name # No extra queries
end
# For more complex relationships
Post.includes(:user, :comments, categories: :posts)
✅ Existence Checks - Performance Matters
# ❌ INEFFICIENT - Loads the complete object in memory
Project.find(1).present? # SELECT * FROM projects WHERE id = 1
# ✅ EFFICIENT - Just checks if it exists in the database
Project.exists?(id: 1) # SELECT 1 FROM projects WHERE id = 1 LIMIT 1
# ✅ ALTERNATIVE - For complex queries
Project.where(id: 1, active: true).exists?
# ⚠️ BE CAREFUL - Loads all records
Project.where(id: 1).any? # SELECT * (can be dangerous with lots of data)
📊 Count vs Size vs Length - Performance Matters
# Scenario: User with many posts
user = User.first
# 1. COUNT - Always runs SQL query
user.posts.count
# => SELECT COUNT(*) FROM posts WHERE user_id = 1
# 2. SIZE - Smart: uses cache if collection was already loaded
user.posts.size
# If posts weren't loaded: SELECT COUNT(*)
# If posts were already loaded: posts.length (no query)
# 3. LENGTH - Forces complete collection loading
user.posts.length
# => SELECT * FROM posts WHERE user_id = 1 (DANGEROUS!)
Rule of thumb:
-
size
: Use by default (99% of cases) -
count
: Use when you need the exact value from database -
length
: Avoid, except for small collections already loaded
🔍 Query Explain - Performance Debugging
# Analyze query performance
User.joins(:posts).where(posts: { published: true }).explain
# IRB result:
# 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=?)
# Look for:
# "SCAN" = Complete table scan (can be slow)
# "SEARCH USING INDEX" = Using index (fast)
# "USING INTEGER PRIMARY KEY" = Primary key search (awesome)
🔗 Merge - Combining Scopes Nicely
merge
lets you combine conditions from different models in a clean way:
# Scenario: Published posts from active users
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
# ❌ Verbose way
Post.joins(:user)
.where(published: true)
.where(users: { active: true, premium: true })
# ✅ Clean way with merge + scopes
Post.published
.joins(:user)
.merge(User.active)
.merge(User.premium)
# ✅ Merge also works with where directly
Post.joins(:user)
.merge(User.where(active: true))
.where(published: true)
# ✅ Combining multiple conditions
Post.joins(:user, :comments)
.merge(User.where('created_at > ?', 1.year.ago))
.merge(Comment.where('created_at > ?', 1.week.ago))
Hands-on Console Examples
Now that you've populated the data with rails db:seed
, try these commands:
# Test basic relationships
user = User.first
user.posts.published
# Test includes
Post.includes(:user, :categories, :comments).first
# Count by category
Category.joins(:posts).group('categories.name').count
# Example of pluck vs select
Category.where(name: "Ruby on Rails").pluck(:id)
Category.where(name: "Ruby on Rails").select(:id)
# Test existence checks
Post.exists?(title: "Complete Guide to Active Record Queries")
Post.where(published: true).any?
# Performance comparison
user.posts.count
user.posts.size
Wrap Up
These simple tips can make a huge difference in your Rails code performance and readability. Remember:
- ✅ Use
pluck
when you need specific values and won't continue with other queries - ✅ Use
select
when using in subqueries or continuing the query chain - ✅ Use
exists?
instead of loading objects just to check existence - ✅ Always prefer
includes
to avoid N+1 - ✅
size
is usually the best choice between count/size/length - ✅
merge
makes your queries more readable and reusable
💡 Tip: If this content helped you, share it with other developers!
Top comments (0)