Olumuyiwa Osiname
Posted on November 30, 2024
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 😄)
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
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'
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?
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")
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
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]
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
Posted on November 30, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.