はじめに
「present?メソッドとany?メソッドだったらany?メソッドが高速なのか・・・なぜだろう?」
この疑問を解決すべく、present?メソッドとany?メソッドの処理の違いについて調べてみました。
環境
macOS Catalina Version 10.15.4
Ruby: 2.7.0
Ruby on Rails: 6.0.2.2
present?メソッド、any?メソッドの違い
どちらもテーブルにデータが存在するかを確認するメソッドです。
けれども内部処理はどちらも異なっています。
侍エンジニアブログさんの、any?メソッド記事の一部を抜粋します。
present?メソッドとany?メソッドとの違いについて紹介します。
結論からいうとany?メソッドの方が高速です。
present? → 全てのデータを取得する
any? → 1件のみデータを取得する
Railsが実行する
SQLを比較して違いを確認してみましょう。
> Sample.where(name:"侍1").present?
Sample Load (0.4ms) SELECT `samples`.* FROM `samples` WHERE `samples`.`name` = '侍1'
=> true
> Sample.where(name:"侍1").any?
Sample Exists (0.4ms) SELECT 1 AS one FROM `samples` WHERE `samples`.`name` = '侍1' LIMIT 1
=> true
このように、SQLの最後にLIMIT 1 が付与されています。
any?メソッドがpresent?よりも優れているように見えます。
ではなぜこのような処理になっているのか、Ruby on Railsのコードを追ってみました。
なお、modelは侍エンジニアブログさんと同様のSampleモデル(stringのname属性のみ)を作成しています。
present?メソッドの内部処理
present?メソッド実行時のコードの場所はblank.rbとなっています。
このコードを追ってみます。
>Sample.where(name: "侍1").method(:present?).source_location
=> ["/Users/xxxxx/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/activesupport-6.0.2.2/lib/active_support/core_ext/object/blank.rb", 25]
def present?
!blank?
end
alias :loaded? :loaded
def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
@klass = klass
@table = table
@values = values
@offsets = {}
@loaded = false
@predicate_builder = predicate_builder
@delegate_to_klass = false
end
def records # :nodoc:
load
@records
end
def load(&block)
unless loaded?
@records = exec_queries(&block)
@loaded = true
end
self
end
def blank?
records.blank?
end
よってpresent?メソッドを呼び出した場合、クエリ未発行ならば、exec_queryメソッドでクエリを発行します。
このクエリは、指定されたテーブルの全レコード検索となります。
その後、結果に応じてblank?を行います。
もしクエリ発行済であれば、クエリを発行せずにblank?を行います。
any?の内部処理
any?メソッド実行時のコードの場所はrelation.rbとなっています。
このコードを追ってみます。
>Sample.where(name: "侍1").method(:any?).source_location
=> ["/Users/xxxxx/.rbenv/versions/2.7.0/lib/ruby/gems/2.7.0/gems/activerecord-6.0.2.2/lib/active_record/relation.rb", 277]
def empty?
return @records.empty? if loaded?
!exists?
end
def any?
return super if block_given?
!empty?
end
def load(&block)
unless loaded?
@records = exec_queries(&block)
@loaded = true
end
self
end
def exists?(conditions = :none)
if Base === conditions
raise ArgumentError, <<-MSG.squish
You are passing an instance of ActiveRecord::Base to `exists?`.
Please pass the id of the object by calling `.id`.
MSG
end
return false if !conditions || limit_value == 0
if eager_loading?
relation = apply_join_dependency(eager_loading: false)
return relation.exists?(conditions)
end
relation = construct_relation_for_exists(conditions)
skip_query_cache_if_necessary { connection.select_one(relation.arel, "#{name} Exists?") } ? true : false
end
よってany?メソッドを呼び出した場合、毎回exists?メソッドにて、テーブルにレコードが1件あるかどうかクエリを発行します。
その後、結果を確認しています。
もしload変数がtrueになった場合は、クエリ発行せずに結果を確認するのみとなります。
まとめ
最初に記載した疑問の答えです。
any?メソッドがpresent?よりも優れているように見えます。
ではなぜこのような処理になっているのか、Ruby on Railsのコードを追ってみました。
present?メソッドの場合、初回のみ全レコードを取得するクエリを発行して判定します。
2回目以降の呼び出しはクエリを発行せずに判定します。
any?メソッドの場合、基本的に毎回、テーブルから1件レコードを取得するクエリを発行して判定します。
このため、全レコードを取り出さないany?メソッドが高速と言われているだと思われます。
ご指摘等あればコメントにご記載をお願い致します。
参考記事
【Rails入門】any?メソッドの便利な使い方を紹介
RailsのActiveRecord::FinderMethodsのSQLクエリ発行の有無について調べる
ActiveRecord::QueryMethodsのselectメソッドについて深掘りしてみた
週刊Railsウォッチ(20191216前編)Rails 6.0.2がリリース、平成Ruby会議01開催、古いRailsのfindメソッド置き換えほか
RailsのArelを調査してみた