When working with Rails, one of the easiest ways to improve performance is to avoid N+1 queries as much as possible.
But what is an N+1 query? Here’s a quick example: let’s say you are rendering a list of 20 books, and you want to show author details on each line. If you don’t preload the authors, accessing book.author.favorite_ice_cream
will trigger one additional query per book, making a total of 21 queries.
Strict loading is a Rails feature that allows avoiding N+1 queries without adding any extra dependencies to the project.
How does strict loading help avoid N+1 queries?
Rails lazy loads resources by default, which means that when accessing a resource that hasn’t been preloaded, Rails queries the database on demand, which can lead to N+1 queries.
Example
class Movie < ApplicationRecord
has_many :reviews
end
movies = Movie.all
movies.each do |movie|
puts movie.reviews.size # This triggers an N+1 query for each movie
end
In the above example, we are performing an extra query to fetch the reviews for each movie, which creates an N+1 query problem.
To prevent this issue, we can enable strict loading on the reviews
association in the Movie
model. This will raise an error whenever a review is accessed without being preloaded, forcing us to use eager loading instead of lazy loading.
Here’s how that looks in practice.
class Movie < ApplicationRecord
has_many :reviews, strict_loading: true
end
Now accessing the movie.reviews
will raise a ActiveRecord::StrictLoadingViolationError
if the reviews are not preloaded.
movie = Movie.first
movie.reviews # Raises ActiveRecord::StrictLoadingViolationError
To avoid the error, all we need to do is include or preload the reviews in the initial query.
movie = Movie.first.includes(:reviews)
movie.reviews
Enabling strict loading
Strict loading can be enabled on multiple levels such as:
Model level strict loading
It is possible to enable strict loading for a model and all of its associations by setting the strict_loading_by_default
attribute to true
.
class Movie < ApplicationRecord
self.strict_loading_by_default = true
has_many :reviews
end
class Review < ApplicationRecord
belongs_to :movie
end
In this case, calling Movie.first.reviews
without including the reviews
in the initial query will raise an error.
Association level strict loading
In cases where you want strict loading only on a specific association, you can do so by marking the setting strict_loading: true
on the targeted association.
class Movie < ApplicationRecord
has_many :reviews, strict_loading: true
end
Global strict loading configuration options
Rails offers the following global configuration options for strict loading:
Configuring strictloadingmode
By default config.active_record.strict_loading_mode
is set to :all
, meaning that any lazy loading attempt will raise an error. However, you can change it to :n_plus_one_only
, which will only raise errors when associations are loaded in a way that leads to N+1 queries like this:
# config/environments/development.rb
config.active_record.strict_loading_mode = :n_plus_one_only
I recommend using :n_plus_one_only
since an error will only be raised when the association is loaded in a way that leads to N+1 queries.
Showing strict loading violation only in logs
By default, strict loading will raise an error if there’s a violation, but it can be turned off by explicitly logging the violations.
# config/environments/production.rb
config.active_record.action_on_strict_loading_violation = :log
It’s recommended to use this option in production environments to avoid crashing the application when strict loading is violated.
Enabling strict loading validations for all models
Strict loading can be enabled for all the models via configuration:
# config/application.rb
config.active_record.strict_loading_by_default = true
You can also disable it in the console via configuration:
# config/application.rb
console do
ActiveRecord::Base.strict_loading_by_default = false
end
Disabling strict loading during test setups for FactoryBot
By default, if strict_loading is enabled for a model, any test setup that accesses an association without eager loading will fail.
This can be annoying because factories are just setting up data, and strict loading is primarily meant to catch issues in controllers/views.
You can disable strict loading via configuration:
# spec/factories.rb
FactoryBot.define do
after :build do |record|
if record.is_a? ActiveRecord::Base
record.strict_loading! false
end
end
after :stub do |record|
if record.is_a? ActiveRecord::Base
record.strict_loading! false
end
end
end
Without this configuration, your test setup could fail due to strict loading violations before the actual test runs, which is not what we want.
Conclusion
Using strict loading in your Rails app can help prevent N+1 queries and improve application performance without adding any extra dependency to your project, but when using it, setting the proper configurations is crucial to avoid unexpected behavior.