🐘

【Rails】ActiveRecord の遅延評価について

2024/04/21に公開

概要

ActiveRecord の where や find などの検索系のメソッドが、
どのタイミングでデータベースへクエリを実行するかを解説します。

クエリを実行するタイミングを理解することで、N+1問題を防ぎ、パフォーマンスを考慮した実装ができるようになるので、ぜひ理解を深めていきましょう!

ActiveRecord の遅延評価とは

例えば以下のコードの場合、クエリの実行タイミングは、1, 2, 3 のどの間になるでしょうか?

controllers/api/v1/articles_controller.rb
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 のタイミングで実行されます。

controllers/api/v1/articles_controller.rb
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件のエンティティを検索するメソッドの場合、そのモデルの単一のインスタンスを返します。

https://railsguides.jp/active_record_querying.html

ActiveRecord::FinderMethods に実装されているメソッドが、インスタンスまたは、インスタンスの配列を返すようです。
https://qiita.com/ykamez/items/0c81a33ec1b90219d541

表にするとこんな感じです。

メソッドの例 SQL 実行のタイミング
遅延評価 where, group そのデータが必要になったタイミング(each など実行されたとき)
即時評価 find, find_by そのメソッドが実行されたとき

Discussion