【Rails】ActiveRecord の遅延評価について
概要
ActiveRecord の where や find などの検索系のメソッドが、
どのタイミングでデータベースへクエリを実行するかを解説します。
クエリを実行するタイミングを理解することで、N+1問題を防ぎ、パフォーマンスを考慮した実装ができるようになるので、ぜひ理解を深めていきましょう!
ActiveRecord の遅延評価とは
例えば以下のコードの場合、クエリの実行タイミングは、1, 2, 3 のどの間になるでしょうか?
class Api::V1::ArticlesController < Api::V1::BaseController
def index
puts "1"
articles = Article.all
puts "2"
render json: { data: articles }
puts "3"
end
ログを確認すると、以下のようになります。
Started GET "/api/v1/articles" for 172.18.0.1 at 2024-04-21 09:03:29 +0000
Processing by Api::V1::ArticlesController#index as HTML
1
2
Article Load (15.3ms) SELECT `articles`.* FROM `articles`
↳ app/controllers/api/v1/articles_controller.rb:8:in `index'
3
articles = Article.all
で実行されると思って、1と2の間に実行されると考えた方が多いのではないでしょうか。
しかし実際は、 render json: { data: articles }
のコードでクエリが実行されていることが分かります。
このように、クエリの作成段階ではなく、データが実際に必要とされるタイミングで実行されることを、遅延評価と呼びます。
render メソッドで json 形式にしてフロントに返す際に、 to_json
メソッドか何かが内部で実行されるタイミングで、実際にクエリが実行されます。
ちなみに以下のようなコードだと、 articles.each
のタイミングで実行されます。
class Api::V1::ArticlesController < Api::V1::BaseController
def index
puts "1"
articles = Article.all
puts "2"
articles.each do |article|
puts "3"
puts article.title
end
puts "4"
render json: { data: articles }
puts "5"
end
Started GET "/api/v1/articles" for 172.18.0.1 at 2024-04-21 09:15:14 +0000
Processing by Api::V1::ArticlesController#index as HTML
1
2
Article Load (1.3ms) SELECT `articles`.* FROM `articles`
↳ app/controllers/api/v1/articles_controller.rb:8:in `index'
3
hoge
3
fuga
3
piyo
4
5
ここではサラッと説明しますが、今回の render json: { data: articles }
の部分で
クエリが実行されていないのは、キャッシュが行われているためです。
Rails では、同じクエリをデータベースで再度実行しないように結果をキャッシュしています。
ActiveRecord::Relation について
では、articles = Article.all
は何をしているのでしょうか?
これは ActiveRecord::Relation
のインスタンスを返しています。
> Article.all.class
=> Article::ActiveRecord_Relation
ActiveRecord::Relation
には以下のような特徴があります。
- 遅延評価
- チェーン可能
遅延評価は先程述べた通りです。
チェーン可能とは ActiveRecord::Relation
インスタンスにさらにメソッドチェーンをすることで、
クエリを作成していくことができるということです。いわゆる、「Ruby っぽい書き方」でクエリを表現できます。
SELECT *
FROM articles
WHERE title = 'hoge'
OR title = 'fuga'
この SQL は以下のように Rails では書くことができます。
Article.where(title: 'hoge').or(Article.where(title: 'fuga'))
遅延評価されずに即データベースから呼び出すメソッド
find
, find_by
, exists?
などの ActiveRecord::FinderMethods
に実装されているメソッドは遅延評価せず、即データベースでクエリを実行します。
検索メソッドにはwhereやgroupといったコレクションを返すものもあれば、ActiveRecord::Relationインスタンスを返すものもあります。また、findやfirstなど1件のエンティティを検索するメソッドの場合、そのモデルの単一のインスタンスを返します。
ActiveRecord::FinderMethods
に実装されているメソッドが、インスタンスまたは、インスタンスの配列を返すようです。
表にするとこんな感じです。
メソッドの例 | SQL 実行のタイミング | |
---|---|---|
遅延評価 | where, group | そのデータが必要になったタイミング(each など実行されたとき) |
即時評価 | find, find_by | そのメソッドが実行されたとき |
Discussion