ActiveRecord各メソッドのクエリ実行タイミングについて
概要
ActiveRecordの各検索メソッドが実際にクエリを投げ、データベースにアクセスするタイミングについて調べてみた。
書こうと思ったきっかけ
ActiveRecordのどの検索メソッドが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の生成に用いている(元々は外部のライブラリとして開発され、取り込まれた。)
具体例: はまりがちなトラップ
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周りの話はパフォーマンスにも大きな影響を与えるので、引き続き調べていきたい。 調べていく過程で面白かった記事も紹介しておく。
railsdm2018で「ActiveRecordデータ処理アンチパターン」を発表しました - Hack Your Design! ActiveRecordであるあるなアンチパターンをまとめた発表。
第43回 Rails 3を支える名脇役たち その1 - Arel -:Ruby Freaks Lounge|gihyo.jp … 技術評論社
ActiveRecordを支える技術 - Arelとは何者なのか? (全5回) その1 - TIM Labs ActiveRecord::Relationのクエリ生成を司るArelに関してまとめた記事。
RailsのActiveRecord::FinderMethodsのSQLクエリ発行の有無について調べる - Qiita 今回の記事で取り上げたfinde_methodsのメソッドに関して、詳細にまとめてある記事。