DEV Community

Rodrigo Barreto for Vídeos de Ti

Posted on

Tips and Tricks Ruby on Rails

Where This Came From

This content was inspired by:

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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

🛠️ 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"
Enter fullscreen mode Exit fullscreen mode

🔄 Reload

# Reload code without leaving the console
reload!
Enter fullscreen mode Exit fullscreen mode

🔍 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)
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

✅ 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)
Enter fullscreen mode Exit fullscreen mode

📊 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!)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

🔗 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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)