In this article, I want to share my journey with ActiveRecord. When I first started with Rails, I thought ActiveRecord was just "magic" to save data to the database. But after working on larger projects, I realized there is so much more to it.
It is very easy to write code that works, but it is also very easy to write code that makes your app slow. Here are the levels of ActiveRecord knowledge, moving from simple queries to more advanced optimizations.
LEVEL 1: The Basics (Chaining)
Most beginners know find and where. But the cool thing about ActiveRecord is that you can chain these methods together endlessly until you actually ask for the data.
For example, if you want to find users who are active and signed up recently:
# Instead of doing this
users = User.where(active: true)
recent_users = users.where('created_at > ?', 1.day.ago)
# You can chain it nicely
User.where(active: true).where('created_at > ?', 1.day.ago)
This generates only one SQL query. Rails is smart enough to wait until the last moment to touch the database.
LEVEL 2: Cleaning up with Scopes
Very often I find myself repeating the same where conditions in multiple controllers.
For example: Post.where(published: true).where(deleted: false).
It is annoying to type this every time. To fix this, we should use Scopes.
You define them in your model, and they act just like class methods.
# post.rb
class Post < ApplicationRecord
scope :live, -> { where(published: true, deleted: false) }
scope :recent, -> { where('created_at > ?', 1.week.ago) }
end
Now your controller code looks much cleaner:
# posts_controller.rb
def index
@posts = Post.live.recent
end
That's pretty much it. It makes your code look like plain English.
LEVEL 3: Solving the N+1 Problem (Advanced)
This is the most common mistake I see in Rails apps. It happens when you load a parent record, and then loop through its children.
Imagine you list 10 Posts, and for each post, you want to show the Author's name.
# controller
@posts = Post.all
# view
<% @posts.each do |post| %>
<%= post.user.name %>
<% end %>
This looks fine, right? Wrong.
Rails will run 1 query to get the Posts, and then 10 more queries (one for each post) to get the User. If you have 100 posts, that's 101 queries. This kills your app performance.
STEP 1: Use includes
To fix this, we need to tell ActiveRecord to fetch everything at once.
@posts = Post.includes(:user).all
Now Rails will only run 2 queries:
- Fetch all Posts.
- Fetch all Users associated with those IDs.
It is a huge difference in speed. Always check your server logs. If you see many queries selecting from the same table over and over, you need includes.
LEVEL 4: Pluck vs Map (Optimization)
Sometimes you don't need the whole database object. You just need an array of IDs or Names.
A lot of developers do this:
# This loads all User objects into memory (heavy)
User.all.map(&:email)
This instantiates a heavy ActiveRecord object for every user, just to grab one string. It consumes a lot of memory.
Instead, use pluck:
# This only fetches the specific column from DB (fast)
User.pluck(:email)
This goes straight to the database and returns an array of strings. It ignores the model logic completely, which is much faster if you just need raw data.
LEVEL 5: Use exists? instead of present?
This is a small tip but very useful.
If you just want to check if a record exists, but you don't need to use the data, don't load the record.
# Bad: Loads the post into memory just to check if it's there
if Post.where(id: params[:id]).present?
...
end
# Good: Runs a smart SQL "LIMIT 1" query
if Post.exists?(params[:id])
...
end
exists? returns a boolean and stops querying as soon as it finds one match.
That's it for today. Mastering these few tricks will make your Rails applications much faster and your code much cleaner.
Top comments (0)