• No results found

Rails Example #4 - Reducing Repeated Queries

In document Debugging Ruby (Page 32-37)

In this section we’ll cover:

• Includes queries

• Pagination

This example app is a slight modifications on the earlier blogging application. In this application, we have aPostmodel whichhas_many :comments, and thatCommentmodelhas_many :users. Before we start this application, we’re going to need to run this command to set up some test data:

rake db:seed

All this task will do is run the code within the db/seeds.rb file within the context of our Rails application’s development environment. This code looks like this:

post = Post.create(:title => "Hello World", :body => "This is the first post.") users = 5.times.map do

User.create(:name => "User#{rand(9999)}") end

1000.times do Comment.create(

:post => post,

:user => users[rand(5)],

:body => "This is just a comment."

) end

This code creates a single post, then 5 randomly numbered users, then 1000 comments with the same post and text, but a randomly chosen user for each comment. Let’s look at what this data makes our application do by firing up the Rails server:

rails s

Going to http://localhost:3000 will show us the familiar list of posts:

Posts

Clicking on this post’s link will now show us the post itself, along with a whole bunch of comments:

Posts

Your numbers will probably⁸be different, but that’s alright. If we switch over to the terminal session whererails sis running, we’ll see a quite large amount of queries that are being run to load our data:

Processing by PostsController#show as HTML Parameters: {"id"=>"1"}

Post Load (0.1ms) SELECT "posts".* FROM "posts" ...

Comment Load (2.8ms) SELECT "comments".* FROM "comments" ...

User Load (0.2ms) SELECT "users".* FROM "users" ...

CACHE (0.0ms) SELECT "users".* FROM "users" ...

User Load (0.2ms) SELECT "users".* FROM "users" ...

CACHE (0.0ms) SELECT "users".* FROM "users" ...

At the top of this mountain of queries is the query to load the post. This needs to happen, otherwise we would not be able to show the post’s data. The second query is one to load the comments, which runs for a similar reason: we need to show the comments. The 1000 queries (don’t count them) that follow are loading all the users for the comments. But why are there 1000 queries when there’s only 5 users to load? Because Rails doesn’t know any better. Luckily for us though, Active Record is caching the results of these queries and fetching the back from the cache when it goes to run that query again. These lines in the output begin withCACHE.

At the end of the output, we’ll see this:

Completed 200 OK in 681ms (Views: 655.3ms | ActiveRecord: 18.4ms)

The entire action is taking 681ms to render, 655ms of that is within the view and 18.4ms is happening within Active Record. A lot of this slowness is due to the number of queries and query cache hits that Rails is undergoing during this request.

Interesting trivia fact: The probability of the numbers not being different is 1 in 10000 to the power of 5, or 1 in 100 quintrillion (1 in 100,000,000,000,000,000,000).

When any action within our application runs, we should try and minimize the database querying needed for that action. Even a single query takes time, and so ideally we would like to reduce it down to 0 queries. Let’s not go all the way to that extreme yet. We should focus our efforts on making Active Record stop performing 1000 lookups for these users.

The way we can fix this problem is with something in Active Record called eager loading. Eager loading will load the required data in as few queries as possible. We can use this within theshow action of thePostsControllerlike this:

def show

@post = Post.includes(comments: :user).find(params[:id]) end

The includesstatement here triggers eager loading for this query. The Hash object passed as the argument to this tells Active Record to eager load thecommentsassociation from thePostmodel, as well as theuserassociation for each comment.

When we reload the page in our browser and look at the queries again, there’s no longer over a thousand of them:

Started GET "/posts/1" for 127.0.0.1 at 2013-11-24 11:52:14 +1100 Processing by PostsController#show as HTML

Parameters: {"id"=>"1"}

... SELECT "posts".* FROM "posts"

WHERE "posts"."id" = ? LIMIT 1 [["id", "1"]]

... SELECT "comments".* FROM "comments"

WHERE "comments"."post_id" IN (1) ... SELECT "users".* FROM "users"

WHERE "users"."id" IN (1, 3, 2, 5, 4)

There are now only 3 queries for the data that needs to be displayed on this page: one for the post, one for the comments and one for all the users for all the comments. The eager loading here has altered the query for the comments to load it using anINquery, rather than the query it ran before:

... SELECT "comments".* FROM "comments"

WHERE "comments"."post_id" = ? [["post_id", 1]]

This doesn’t make too much of a different in the query speeds. The two queries are going to produce the same result. The major difference here is that the 1000 queries for the users for the comments has now been reduced to one simple query:

... SELECT "users".* FROM "users"

WHERE "users"."id" IN (1, 3, 2, 5, 4)

We can also see that the page which once took around 700ms to load (650ms in the view, 25ms in Active Record), is now loading much, much faster:

Completed 200 OK in 108ms (Views: 51.0ms | ActiveRecord: 3.9ms)

The page is now loading seven times faster with this one easy trick. That’s great!

TODO

• Pagination

• Fragment caching

• (?) Mention the Bullet gem

Not all exceptions known to humankind have been documented in this book so far. There will be more than these which pop up in the applications that you will write. By default, when users cause an exception to happen within a Rails application, this is what they’ll see:

500 Error Page

Exceptions will happen in your applications, because nobody writes perfect code. Sometimes these exceptions might happen under a set of very, very specific circumstances and only for one user.

Unless that user contacts you specifically, you may never know that this exception is happening.

That’s where exception notification services come into play. These range from the basic exception_-notification gem (http://smartinez87.github.io/exception_exception_-notification/) which you can install and configure to email you whenever an exception happens, all the way up to the large scale New Relic which does much much more than exception notifications. There are others too, such as Airbrake (http://airbrake.io) and Honeybadger (honeybadger.io).

Advanced Rails Example #1 - Broken Devise

In document Debugging Ruby (Page 32-37)

Related documents