Avoiding N+1 queries the Railsy way with strict loading

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.

Additional resources: