今回は関連付けによる遅延読み込み(Lazy Loading)を防ぐことができる機能「strict_loading」を使ってみたのでまとめたいと思います。遅延読み込みを防ぐことでパフォーマンスに大きく影響するN+1問題を解決できるため、非常に便利な機能が導入されたと言えますね。

strict_loadingとは
strict_loadingとは、Rails6.1からActiveRecordに追加された機能で、関連付け(アソシエーション)による遅延読み込み(Lazy Loading)を防ぐことができる機能です。これにより、アプリケーションのパフォーマンスに影響を与えるN+1問題を回避することができます。
To prevent lazy loading of associations, strict_loading will cascade down from the parent record to all the associations to help you catch any places where you may want to preload instead of lazy loading.
*N+1問題を検知する手段として他にはgem「bullet」があります。

strict_loadingを使ってみる
では実際にアプリケーションにstrict_loadingを導入し、どのように遅延読み込み(Lazy Loading)を検知してくれるのか確かめてみます。
新規アプリケーションの作成
% rails new strict-loading-app -d mysql
% cd strict-loading-app
% rails db:create
- アプリケーション名: strict-loadnig-app
- Ruby: 3.1.2
- Rails: 7.0.3
- DB: MySQL
User/Tweetモデルの作成
UserとTweetモデルをScaffoldの機能を用いて作成します。
% rails g scaffold User name:string
% rails g scaffold Tweet title:string user:references
% rails db:migrate

関連付け(アソシエーション)の定義
UserとTweetは1対多の関係になるため、以下のように関連付けを定義します。
class User < ApplicationRecord
has_many :tweets
end
class Tweet < ApplicationRecord
belongs_to :user
end
初期データの投入
遅延読み込み(Lazy Loading)を発生させるため初期データを投入します。
% rails c
> user = User.create!(name: '田中太朗')
> user.tweets.create!(title: '今日の朝ご飯')
> user.tweets.create!(title: '今日の昼ご飯')
> user.tweets.create!(title: '今日の夜ご飯')
strict_loadingの定義
関連付け(アソシエーション)にstrict_loadingを定義します。これにより、レコードを取得する際に遅延読み込みが発生した場合はエラーが表示されるようになります。
class User < ApplicationRecord
has_many :tweets, strict_loading: true
end
それぞれのアソシエーションに定義せずとも、以下のようにモデル単位でまとめてstrict_loadingを適用することも可能です。
class User < ApplicationRecord
self.strict_loading_by_default = true
has_many :tweets
end
アプリケーション全体に適用する場合はapplication_record.rb
に定義します。
class ApplicationRecord < ActiveRecord::Base
self.strict_loading_by_default = true
primary_abstract_class
end
N+1の検知
では実際にN+1が検知されるのか検証してみます。
User.last.tweets
として関連先を取得すると、以下のように「#<Tweet::ActiveRecord_Associations_CollectionProxy:0x3660>
」が出力されます。
% rails c
> User.last.tweets
User Load (0.2ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
=> #<Tweet::ActiveRecord_Associations_CollectionProxy:0x3660>
一方で遅延読み込み(Lazy Loading)を回避するincludesメソッドを定義すると、エラーは吐かれずに関連先を取得することができました。
% rails c
> User.includes(:tweets).last.tweets
User Load (0.4ms) SELECT `users`.* FROM `users` ORDER BY `users`.`id` DESC LIMIT 1
Tweet Load (0.5ms) SELECT `tweets`.* FROM `tweets` WHERE `tweets`.`user_id` = 2
=> [#<Tweet:0x000000010a98abb0
id: 1,
title: "今日の朝ご飯",
user_id: 2,
created_at: Mon, 20 Jun 2022 14:35:46.728524000 UTC +00:00,
updated_at: Mon, 20 Jun 2022 14:35:46.728524000 UTC +00:00>,
#<Tweet:0x000000010a98aa48
id: 2,
title: "今日の昼ご飯",
user_id: 2,
created_at: Mon, 20 Jun 2022 14:35:54.225074000 UTC +00:00,
updated_at: Mon, 20 Jun 2022 14:35:54.225074000 UTC +00:00>,
#<Tweet:0x000000010a98a8e0
id: 3,
title: "今日の夜ご飯",
user_id: 2,
created_at: Mon, 20 Jun 2022 14:35:58.030737000 UTC +00:00,
updated_at: Mon, 20 Jun 2022 14:35:58.030737000 UTC +00:00>]
このようにstrict_loadingの機能を用いることで、簡単に関連付け(アソシエーション)による遅延読み込み(Lazy Loading)を防ぐことができます。
エラーではなくログに表示する
先ほどの例では、遅延読み込み(Lazy Loading)が発生した場合はエラーになりましたが、エラーにせずログに表示することも可能です。
config/environments/development.rb
に以下の1行を追加します。
config.active_record.action_on_strict_loading_violation = :log
参考
今回は関連付けによる遅延読み込み(Lazy Loading)を防ぐことができる機能「strict_loading」についてまとめました。導入も簡単なので、gem「bullet」からの乗り換えを検討してみても良さそうです。