
Rails 6.1 was released on December 9, 2020 with great new features. In this blog post, we will see the overview of the feature “Strict Loading Association” – https://github.com/rails/rails/pull/37400 , https://github.com/rails/rails/pull/38541 , https://github.com/rails/rails/pull/39491.
How it works:
With lazy loading approach, the data will be queried from the database when it is actually required. Consider a topic can be associated with many posts and if you want to list all topics and associated posts in the same page, Lazy loading will generate N number of queries to access the posts and 1 query to load the topics info. This will increase number of requests to the database and increases response time of the request. To avoid this performance issues, we are eager loading to associated data.
Strict loading is introduced for developers to helps us caching possible N+1’s. When strict_loading mode is enabled and you are trying to lazy load the data, it will raise ActiveRecord::StrictLoadingViolationError exception.
For example, Please consider the below models:
class
Topic
<
ApplicationRecord
has_many
:posts, dependent: :destroy
end
class
Post
<
ApplicationRecord
belongs_to
:topic
end
Lets try strict_loading mode from rails console:
topic.strict_loading! enables strict_loading mode on the topic object, When the posts association lazy loaded, rails by default raises an exception.
2.7.0 :001 > topic = Topic.first
Topic Load (0.5ms) SELECT "topics".* FROM "topics" ORDER BY "topics"."id" ASC LIMIT $1 [["LIMIT", 1]]
2.7.0 :002 >
2.7.0 :003 > topic.strict_loading?
=> false
2.7.0 :004 > topic.strict_loading!
=> true
2.7.0 :005 > topic.strict_loading?
=> true
2.7.0 :006 > topic.posts
Traceback (most recent call last):
ActiveRecord::StrictLoadingViolationError (`Post` called on `Topic` is marked for strict_loading and cannot be lazily loaded.)
Let’s try eager loading the posts association and check the outcome:
2.7.0 :001 > topic = Topic.first
Topic Load (0.8ms) SELECT "topics".* FROM "topics" ORDER BY "topics"."id" ASC LIMIT $1 [["LIMIT", 1]]
2.7.0 :002 >
2.7.0 :003 >
2.7.0 :004 > topic = Topic.includes(:posts).first
Topic Load (0.7ms) SELECT "topics".* FROM "topics" ORDER BY "topics"."id" ASC LIMIT $1 [["LIMIT", 1]]
Post Load (5.2ms) SELECT "posts".* FROM "posts" WHERE "posts"."topic_id" = $1 [["topic_id", 14]]
2.7.0 :005 > topic.strict_loading!
=> true
2.7.0 :006 > topic.strict_loading?
=> true
2.7.0 :007 > topic.posts
=> #<ActiveRecord::Associations::CollectionProxy [#<Post id: 28, content: "Test Post 1", status: "active", topic_id: 14, created_at: "2021-01-27 18:32:38.281310000 +0000", updated_at: "2021-01-27 18:32:38.281310000 +0000">, #<Post id: 29, content: "Test Post 2", status: "active", topic_id: 14, created_at: "2021-01-27 18:32:43.713495000 +0000", updated_at: "2021-01-27 18:32:43.713495000 +0000”>]
It does return list of posts without any exceptions after eager loading.
Possible ways of enabling strict_loading mode:
1. Enabling on per record basis:
2.7.0 :001 > topic = Topic.first
Topic Load (0.5ms) SELECT "topics".* FROM "topics" ORDER BY "topics"."id" ASC LIMIT $1 [["LIMIT", 1]]
2.7.0 :002 > topic.strict_loading!
=> true
2.7.0 :003 > topic.strict_loading?
=> true
2.7.0 :001 > topic = Topic.strict_loading.first
Topic Load (13.9ms) SELECT "topics".* FROM "topics" ORDER BY "topics"."id" ASC LIMIT $1 [["LIMIT", 1]]
2.7.0 :002 > topic.strict_loading?
=> true
2. Enabling for all the associations defined on specific model:
Setting strict_loading_by_default to true inside the model will enable strict_loading mode for all the associations on the specific model.
class
Topic
<
ApplicationRecord
self.strict_loading_by_default = true
has_many
:posts, dependent: :destroy
end
2.7.0 :001 > topic = Topic.first
Topic Load (1.9ms) SELECT "topics".* FROM "topics" ORDER BY "topics"."id" ASC LIMIT $1 [["LIMIT", 1]]
2.7.0 :002 > topic.strict_loading?
=> true
3. Enabling per association basis:
Strict loading mode can be enabled on the association by passing the argument strict_loading: true while defining the association.
class
Topic
<
ApplicationRecord
has_many
:posts, dependent: :destroy, strict_loading: true
end
2.7.0 :002 > topic = Topic.first
Topic Load (8.8ms) SELECT "topics".* FROM "topics" ORDER BY "topics"."id" ASC LIMIT $1 [["LIMIT", 1]]
2.7.0 :003 > topic.posts
ActiveRecord::StrictLoadingViolationError (`posts` called on `Topic` is marked for strict_loading and cannot be lazily loaded.)
4. Enabling on application wide:
Strict loading mode can be enabled on application level using the configuration – strict_loading_by_default. The below will enable strict_loading mode for all the associations in all the models. Default value of this configuration is false.
development.rb:
config.active_record.strict_loading_by_default = true
Disabling strict loading mode:
You can also disable the strict loading mode using the above mentioned configuration by setting it to “false”, For example, If the strict_loading_by_default is set to true on application.rb (or any environment related configuration) and if you want to disable the strict loading mode for the specific model, you can disable with the below query
This will turn off strict loading for all the association defined on this model:
class Topic < ApplicationRecord
self.strict_loading_by_default = false
has_many :posts, dependent: :destroy
end
Provisions for the actions to be done during the strict_loading mode violation:
By default, an exception will be raised when an association is lazy loaded. Rails adds a configuration named action_on_strict_loading_violation. With the help of this configuration, we can also update the configuration to log the violations instead of raising exception. It accepts the values :raise and :log. It defaults to :log
development.rb:
config.active_record.action_on_strict_loading_violation = :log
Thus we got a great new feature to alert us whenever there is a possible N+1.
Hope this helps you with understanding the working behaviour of strict loading mode.
Gomathi N,
ROR Team,
Mallow Technologies.