Congratulations on coming this far! By now you understand how Ruby on Rails works and you can start making your own applications.
It's time to dive deeper and use some best practices.
In your index.html.erb
, we display all of the comments for each post. We have written a loop like this:
<% @posts.each do |post| %>
...
<% end %>
Notice how we iterate through each post and for each post, we iterate through each comment in the post. We are also querying for the user
of each Post
and Comment
.
Let's open our Instapost application and trigger the posts#index
action (this should be your root_path
). In other words, go to the root directory of your application.
Let's look at our Rails server to examine the performance:
Woah. Look at all of those requests! (As a side note, this request is querying for 13 posts that has 25 comments in total, created by 2 users.)
The application is querying each user
for each post
, it's querying for all of the comments
for each post
, and it is querying for each user
for each comment
. This leads to a long list of queries and is often called the N+1 Problem.
The N+1 problem refers to making a query to fetch the parent, then any number of child queries to fetch the other records.
If the application is still a small application, this won't really affect anything, but once the application starts growing, we need ways to optimize this request.
One technique to accomplish this is eager loading. Eager loading refers to the practice of fetching all related records at once, instead of fetching them as the program encounters the queries.
In other words, with eager loading, we can fetch all of the records that we need to query all at once. This is far more efficient than trying to fetch all of the records one by one. Fetching 100 records in 1 query is faster than issuing 100 queries to fetch 1 query each.
One way to implement eager loading is by using the includes
method. In our posts_controller.rb
, let's modify the index
method to implement this. First, let's eager load the comments
for all of the posts:
def index
@posts = Post.all.order('created_at DESC').includes(:comments)
end
Save the file and render the index page again. Let's look at the differences in the performance:
Hmm...this is better, but it is still making a bunch of request. We can do better.
As we can see, the application is still querying for all of the users. This is because we are querying for the user
of each post. For example, in this piece of code:
<%= post.user.email.split('@').first %>
Let's also eager load our user
for each of our posts:
def index
@posts = Post.all.order('created_at DESC').includes(:comments, :user)
end
Save the file and refresh the page. Let's see the results:
Getting better! The requests are getting shorter and shorter. But it is still querying for a bunch of users. This is because each comment
is also querying for the user
:
<%= comment.user.email.split('@').first %> <%= comment.text %>
Lastly, let's eager load the user
for the comments
. We can do this by modifying the code like such:
def index
@posts = Post.all.order('created_at DESC').includes(:user, comments: :user)
end
Save the file and refresh the page.
Let's look at the results:
Wow! Compare this to what we had initially. As you can see, we clearly have a much smaller request and a better performing application.