Ruby-on-Rails bullet gem tutorial
Prequisites
Tool I will use in this tutorial :
1
2
3
4
5
6
7
8
$> ruby -v
ruby 3.1.2p0 // you need at least version 3 here
$> bundle -v
Bundler version 2.2.11
$> npm -v
8.3.0 // you need at least version 7.1 here
$> yarn -v
1.22.10
Fresh Rails app - no bullet gem yet
Then let’s go at the root of your usual workspace, and start to build a fresh new Rails app :
1
2
3
4
5
mkdir bookapp && cd bookapp
echo "source 'https://rubygems.org'" > Gemfile
echo "gem 'rails', '7.0.4'" >> Gemfile
bundle install
bundle exec rails new . --force --minimal
Side note Notice I use here the --minimal
flag - see options here. We don’t need advanced options of Ruby-on-Rails to show and solve the problem, so let’s take the simplest way.
Create models
Let’s say we have an “Author” table.
1
$/bookapp> bin/rails generate model Author name:string --no-test-framework --no-timestamps
A migration file was created under db/migrate/20221219180435_create_authors.rb
- the number is a timestamp, of course you will have another one.
1
2
3
4
5
6
7
8
# Under db/migrate/20221219180435_create_authors.rb
class CreateAuthors < ActiveRecord::Migration[7.0]
def change
create_table :authors do |t|
t.string :name
end
end
end
Let’s say we have some books with a title. Books are written by Author(s).
1
$/bookapp> bin/rails generate model Book title:string author:references --no-test-framework --no-timestamps
A migration file was created under db/migrate/20221219180441_create_books.rb
:
1
2
3
4
5
6
7
8
9
# db/migrate/20221219180441_create_books.rb
class CreateBooks < ActiveRecord::Migration[7.0]
def change
create_table :books do |t|
t.string :title
t.references :author, null: false, foreign_key: true
end
end
end
Ensure each model has a correct relationship, and add missing lines if required. You should end up with this :
1
2
3
4
# app/models/book.rb
class Book < ApplicationRecord
belongs_to :author
end
1
2
3
4
# app/models/author.rb
class Author < ApplicationRecord
has_many :books # add this line
end
Create the database
1
$> bin/rails db:create db:migrate
Seed data
The seed.rb
file allows us to put some initial data in our database, so let’s rely on it…
1
2
3
4
5
6
7
8
9
# inside db/seed.rb file
Book.destroy_all
Author.destroy_all
deaubonne = Author.create({ name: "Francoise Deaubonne" })
Book.create({title: "Birth of ecofeminism", author: deaubonne})
Book.create({title: "Feminism or death", author: deaubonne})
Book.create({title: "Verlaine and Rimbaud", author: deaubonne})
And launch :
1
$> bin/rails db:seed
Show me the N+1 queries problem, please
Launch the rails console (bin/rails console
)
1
2
3
4
5
6
7
8
9
10
11
12
Book.all.each { |book| puts "#{book.title} was written by #{book.author.name}" }
Book Load (0.1ms) SELECT "books".* FROM "books"
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Birth of ecofeminism was written by Francoise Deaubonne
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Feminism or death was written by Francoise Deaubonne
Author Load (0.1ms) SELECT "authors".* FROM "authors" WHERE "authors"."id" = ? LIMIT ? [["id", 1], ["LIMIT", 1]]
Verlaine and Rimbaud was written by Francoise Deaubonne
=>
[#<Book:0x0000000107037a20 id: 1, title: "Birth of ecofeminism", author_id: 1>,
#<Book:0x000000010705f278 id: 2, title: "Feminism or death", author_id: 1>,
#<Book:0x000000010705f1b0 id: 3, title: "Verlaine and Rimbaud", author_id: 1>]
You have 4 times the “SELECT” instruction in database.
But you wanted only 3 objects (the 3 books).
This is known as the N+1 queries problem.
Add the bullet gem
The bullet gem has an official repository on GitHub.
Open the Gemfile and add
1
gem "bullet", group: "development"
And open your command line.
1
2
$> bundle install
$> bundle exec rails generate bullet:install
in your config/environments/development.rb
, notice that the following lines were added:
1
2
3
4
5
6
7
8
9
10
11
12
13
Rails.application.configure do
config.after_initialize do
Bullet.enable = true
Bullet.alert = true
Bullet.bullet_logger = true
Bullet.console = true
Bullet.rails_logger = true
Bullet.add_footer = true
end
#...
end
Add a controller, a route, a view
Modify routes.rb as follow :
1
2
3
Rails.application.routes.draw do
get "welcome/index"
end
Add a controller
1
2
3
4
5
6
7
8
class WelcomeController < ApplicationController
# Add this method
def index
@books = Book.all
end
end
Add app/views/welcome/index.html.erb
1
2
3
4
5
6
7
8
9
<h1>Hello</h1>
<% @books.each do |book| %>
<div>
<div><%= book.title %></div>
<div><%= book.author.name %></div>
<div> </div>
</div>
<% end %>
Launch and view the N+1 detection
Open your browser at localhost:3000/welcome/index
You will also find some logs under log/bullet.log
with:
1
2
3
4
5
6
2022-12-19 19:38:43[WARN] user: david
GET /welcome/index
USE eager loading detected
Book => [:author]
Add to your query: .includes([:author])
Call stack
Solution to the problem
Notice that the bullet gem is not here to solve the problem, it only shows you where it happens, and how it could be solved.
In our case, the .includes method of the Rails API was the solution.
We should have written in the controller :
1
2
3
4
5
6
7
8
class WelcomeController < ApplicationController
# Modify this method
def index
@books = Book.includes(:author)
end
end
Which gives the desired output in the browser :
Conclusion
The bullet gem is very helpful to detect potentially long and slow SQL queries. It is elegant and non-invasive, being triggered in the development (and eventually test-) environment only.