LoginSignup
58

More than 3 years have passed since last update.

ActiveRecord各メソッドのクエリ実行タイミングについて

Last updated at Posted at 2019-05-06

概要

ActiveRecordの各検索メソッドがどのタイミングで実際にリクエストを投げるのか不明だったので調べてみた。
まず前提として、メソッドは大きく以下の二種類に分けられる。

  • DBへのリクエストを発行するメソッド
  • DBへのリクエストは発行せず、ActiveRecord::Relationのオブジェクトを返すメソッド

結論

まずはじめに結論から書くと、以下の違いが、リクエストの有無を決めている。

  • ActiveRecord::FinderMethodsに実装されているメソッド
    find, find_by, take, first, last, exists?
    すぐにクエリを発行し、データベースにアクセスし、レコード(Modelのインスタンス or インスタンスの配列)を返す。

  • それ以外の検索メソッド(where, limit, など)
    ActiveRecord::Relationのオブジェクトを返し、実際にデータが必要になるタイミングまでデータベースにはアクセスしない。遅延評価(Lazy Evaluation)である。

ドキュメント: ActiveRecord::FinderMethods

ソースコード: rails/finder_methods.rb at master · rails/rails · GitHub

そもそもActiveRecord::Relationとは?

クエリを生成するための情報を保持し、メソッドチェーンでつなげることができるため、再利用性が高く、便利なものになっている。
一方で、理解して使わないと、意図していないクエリを組み立ててしまい、パフォーマンスの悪化などを招くこともある。

内部実装としては、arelというライブラリをSQLの生成に用いている(元々は外部のライブラリとして開発され、取り込まれた。)

具体例: はまりがちなトラップ

to_aの罠

以下のようなコードを書いた時、whereで返ってきたRelationに対してto_aを呼び出しているため、その段階でRubyの配列になっているため、その段階でデータベースにアクセスが走ってしまうこと、また既に配列になってしまっているため、ActiveRecordのメソッドであるwhereは使えないことに注意したい。

new_users = User.where('created_at > ?', "2019-04-01").to_a
new_users.where(...)
# => NoMethodError: undefined method `where' for #<Array:0x00007fa6c7575df8>

limitとtakeの使い分け

上でも述べたようにtakeはActiveRecord::FinderMethodsのメソッドであり、データベースにアクセスを行い、レコードを取得する。
そのためlimitと同じようなものだと思って使うと誤った挙動を招くことがある。

rails consoleで実行してみると、limitはデータベースにアクセスをせず、takeはデータベースにアクセスをしていることがわかる。
(※メソッドの後に;をつけることで、即時評価されてしまうことを防いでいる。)

> Book.limit(3);
> Book.take(3);
  Book Load (0.5ms)  SELECT  "books".* FROM "books" LIMIT $1  [["LIMIT", 3]]

例えば以下のようなケースを考えてみる。
2019年4月1日以降に作成されたレコードを更新日時順が新しい順に1000件取得し、さらにそのレコードを分析したいという要件があった時、以下のように書くことで再利用性を高めることができ、limitメソッドを使っているため、実際に評価されるまで、クエリは実行されない。

rel  = Book.where('created_at > ?', "2019-04-01").order(created_at: :desc).limit(1000);

しかし、以下のようにtakeを使ってしまうと、その段階で内部でto_aされてしまうため、クエリが実行されてしまうのと、ActiveRecord::Relationではなくなってしまうため、メソッドチェーンが利用できなくなってしまう。

rel  = Book.where('created_at > ?', "2019-04-01").order(created_at: :desc).take(1000);

もちろん要件によっては、limitではなく、takeを使ったほうが正しいケースもあるが、両者の違いを認識して、正しく使う必要がある。

参考にした記事:
- ActiveRecord::QueryMethods#limitとActiveRecord::FinderMethods#take - Qiita

(補足)takeのソースコード

find_take_with_limitの中で、to_aが呼ばれており、このタイミングで評価され、データベースにアクセスしていることがわかる。

    def take(limit = nil)
      limit ? find_take_with_limit(limit) : find_take
    end
(中略)
      def find_take_with_limit(limit)
        if loaded?
          records.take(limit)
        else
          limit(limit).to_a
        end
      end

Ref: rails/finder_methods.rb at master · rails/rails · GitHub


まとめ

ActiveRecordは便利だが、挙動を勘違いして使うと無駄なアクセスが走り、パフォーマンスを低下させる恐れがある。
具体例に関しては、他にも例があげられそうなので、業務などで詰まることがあったら更新していきたい。

ActiveRecord周りの話はパフォーマンスにも大きな影響を与えるので、引き続き調べていきたい。
調べていく過程で面白かった記事も紹介しておく。

参考にした記事

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
58