DEV Community

Olumuyiwa Osiname
Olumuyiwa Osiname

Posted on

Mastering Rails and Ruby: A Collection of Practical Tips for Cleaner Code

1. Avoid N+1 Queries with includes

N+1 query problem occurs when loading associated records in a loop, triggering a query for each associated record. We can solve this by simply preloading associations with includes.

# Bad: Triggers a query for each user
users = User.all
users.each do |user|
  puts user.posts.count
end
# SELECT * FROM users;
# SELECT COUNT(*) FROM posts WHERE user_id = ... (for each user 😢)

# Good: Preloads the association
users = User.includes(:posts)
users.each do |user|
  puts user.posts.size
end
# SELECT * FROM users;
# SELECT posts.* FROM posts WHERE user_id IN (...); (1 query 😄)
Enter fullscreen mode Exit fullscreen mode

2. Use pluck to Fetch Specific Columns

Sometimes we are only interested in specific columns when retrieving a record. Using SQL we would normally SELECT email FROM users WHERE active = TRUE but for some reason we hardly do that when using Active Record.
We can Use pluck to efficiently retrieve specific columns with Active Record.

# Bad: Loads full User objects into memory
emails = User.where(active: true).map(&:email)

# Good: Fetches only the required data
emails = User.where(active: true).pluck(:email)
# SELECT "users"."email" FROM "users" WHERE "users"."active" = TRUE
Enter fullscreen mode Exit fullscreen mode

3. Using Range Syntax

Filter records by date using Rails' range syntax or SQL-like conditions.

posts = Post.where(created_at: ...7.days.ago) 
# Generated SQL: # SELECT "posts".* FROM "posts" WHERE "posts"."created_at" < '2024-11-23 00:00:00'
Enter fullscreen mode Exit fullscreen mode

4. Avoid double negations

For example use present? Instead of !blank?

# Bad: Double negation makes code harder to read 😕 
puts "Name is present" if !user.name.blank? 

# Good: Use `present?` for clarity 👍 
puts "Name is present" if user.name.present?
Enter fullscreen mode Exit fullscreen mode

5. Avoid loading Records when you don't have to.

You can Optimize using exists? Instead of Loading Records

# Bad: Loads full records into memory 😕 
if User.where(email: "test@example.com").first   

# Good: Use `exists?` to avoid loading objects 👍 
if User.exists?(email: "test@example.com")
Enter fullscreen mode Exit fullscreen mode

6. Use with_options for DRY Validations

You can group common validations together in your model. I actually do this a lot for validations that I expect to evolve together

# Bad: Repeating validation options 😕 
class Variant < ApplicationRecord   
  validates :price, numericality: { greater_than_or_equal_to: 0 }
  validates :msrp, numericality: { greater_than_or_equal_to: 0 }
end

# Good: Use `with_options` for grouping 👍 
class Variant < ApplicationRecord   
  with_options numericality: { greater_than_or_equal_to: 0 } do
    validates :price
    validates :msrp
  end
end
Enter fullscreen mode Exit fullscreen mode

7. Using pluck with a Ruby Hash

caveat: This requires 'active_support/core_ext/hash'

# Example 
users = [{id: 1}, {id: 2}]

# Instead of 
users.map { |user| user[:id] }
#=> [1, 2]

# We could simply
users.pluck(:id) #=> [1, 2]
Enter fullscreen mode Exit fullscreen mode

8. Memoizing Associations with has_one

Avoid repeatedly triggering queries for calculated associations by using has_one.

# Bad: Runs a query every time 
class User < ApplicationRecord
  has_many :subscriptions

  def active_subscription
    subscriptions.where(active: true).first
  end
end

# Good: Use `has_one` for efficient association caching

class User < ApplicationRecord
  has_many :subscriptions
  has_one :active_subscription, -> { where(active: true) }, class_name: "Subscription" 
end
Enter fullscreen mode Exit fullscreen mode

Top comments (0)