Optimizing Rails Applications with Bullet gem(N + 1 Problem)

This article will discuss bullet gem which helps you watch n + 1 queries when they are being used unnecessarily, as well as helping you to determine when they should be used.

Let’s first create a project and a few models.

1
2
3
4
5
rails new bullet
rails g scaffold User email name
rails g scaffold Post title body user:belongs_to
rails g scaffold Comment body post:belongs_to
rails db:migrate        

In config/routes.rb, configure the routes.

1
2
3
4
5
6
7
# config/routes.rb

resources :users do
  resources :posts do
    resources :comments
  end
end

Seed in some data.

1
2
3
4
5
6
7
8
9
10
11
# db/seeds.rb

User.create(email: "first_user@email.com", name: "First User")

(1..10).each do |i|
  Post.create(title: "Post no #{i}", body: "This is body of number #{i} post", user_id: User.first.id)
end

(1..10).each do |i|
  Comment.create(body: "This is comment number #{i} post 1", post_id: Post.first.id)
end

Run the following command:

1
rails db:seed

Define relationships.

1
2
3
# app/models/user.rb

has_many :posts
1
2
3
4
# app/models/post.rb

belongs_to :post
has_many :comments
1
2
3
# app/models/comment.rb

belongs_to :post

You will need to change the controllers and views a bit in order to avoid errors.

Controllers

1
2
3
4
5
6
7
8
9
10
11
# app/controllers/posts_controller.rb

def index
  @posts = Post.where(user_id: params[:user_id])
end


def set_post
  @post = Post.where(user_id: params[:user_id]).find(params[:id])
end

Views

1
2
3
4
5
6
7
8
9
10
11
# app/views/posts/index.html.erb

<td><%= link_to 'Show', user_post_path(post.user, post) %></td>
<td><%= link_to 'Edit', edit_user_post_path(post.user, post) %></td>
<td><%= link_to 'Destroy', user_post_path(post.user, post), method: :delete, data: { confirm: 'Are you sure?' } %></td>

# add the following anywhere, we just want to test the functionality

<% post.comments.each do |comment| %>
  <%= comment.body %>
<% end %>
1
2
3
4
# app/views/posts/show.html.erb

<%= link_to 'Edit', edit_user_post_path(@post.user, @post) %> |
<%= link_to 'Back', user_posts_path(@post.user) %>
1
2
3
4
5
6
7
# app/views/comments/index.html.erb

<td><%= link_to 'Show', user_post_comment_path(comment.post.user, comment.post, comment) %></td>
<td><%= link_to 'Edit', edit_user_post_comment_path(comment.post.user, comment.post, comment) %></td>
<td><%= link_to 'Destroy', user_post_comment_path(comment.post.user, comment.post, comment), method: :delete, data: { confirm: 'Are you sure?' } %></td>

<%= link_to 'New Comment', new_user_post_comment_path %>
1
2
3
4
# app/views/comments/show.html.erb

<%= link_to 'Edit', edit_user_post_comment_path(@comment.post.user, @comment.post, @comment) %> |
<%= link_to 'Back', user_post_comments_path(@comment.post.user, @comment.post, @comment) %>

Let’s add dependencies.

1
2
# Gemfile
gem 'bullet', group: 'development'

Run bundle install.

Let’s configure the bullet gem. You will find a lot of options here, but we’ll stick with the basics for now.

1
2
3
4
5
6
7
8
9
# config/environments/development.rb

config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end

Open http://localhost:3000/users/1/posts/ and you will see an alert that warns you for not using eager loading for users and comments. Change the index method of app/controllers/posts_controller.rb to the following to remove the warning:

1
2
3
4
5
# app/controllers/posts_controller.rb

def index
  @posts = Post.where(user_id: params[:user_id]).includes(:user, :comments)
end