概要
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
を使ったほうが正しいケースもあるが、両者の違いを認識して、正しく使う必要がある。
参考にした記事:
(補足)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であるあるなアンチパターンをまとめた発表。 -
[http://gihyo.jp/dev/serial/01/ruby/0043:title]
-
[http://labs.timedia.co.jp/2014/05/activerecord---arel-5-1.html:title]
ActiveRecord::Relationのクエリ生成を司るArelに関してまとめた記事。 -
[https://qiita.com/wakasa51/items/3fe880d0e8e656044778:title]
今回の記事で取り上げたfinde_methodsのメソッドに関して、詳細にまとめてある記事。