Edited at

Active Record の size vs count vs length をコードレベルで見てみた

More than 3 years have passed since last update.

Ruby on Rails の Active Record でいつも何気なく size を使っていたら、なぜかデータベースにエントリがあるはずなのに正しく数を数えられていない症状が起きました。

もう結構出ている話なのですが、size って一度ロードしたデータをロードし直してくれないんですね。

せっかくなのでコードレベルで詳しく調べてみました。


size メソッド

まずは size の定義 :octocat: から。

# Returns the size of the collection by executing a SELECT COUNT(*)

# query if the collection hasn't been loaded, and calling
# <tt>collection.size</tt> if it has.
#
# If the collection has been already loaded +size+ and +length+ are
# equivalent. If not and you are going to need the records anyway
# +length+ will take one less query. Otherwise +size+ is more efficient.
#
# This method is abstract in the sense that it relies on
# +count_records+, which is a method descendants have to provide.
def size
if !find_target? || loaded?
if association_scope.distinct_value
target.uniq.size
else
target.size
end
elsif !loaded? && !association_scope.group_values.empty?
load_target.size
elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
count_records
end
end

関数の始めにいきなりありますね。

if !find_target? || loaded?

if association_scope.distinct_value
target.uniq.size
else
target.size
end
elsif

つまり loaded? だと既にロードしてある targetsize を返すようです。

ちなみに loaded? はこのクラスの親クラスである Association で定義されており、シンプルにインスタンス変数 @loaded の値を返します。


count メソッド

次に count メソッド :octocat: の定義です。

# Count all records using SQL. Construct options and pass them with

# scope to the target class's +count+.
def count(column_name = nil)
relation = scope
if association_scope.distinct_value
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
column_name ||= reflection.klass.primary_key
relation = relation.distinct
end
value = relation.count(column_name)
limit = options[:limit]
offset = options[:offset]
if limit || offset
[ [value - offset.to_i, 0].max, limit.to_i ].min
else
value
end
end

確かに value = relation.count(column_name) でデータベースへ count を発行しており、インスタンス変数とかの保存しているところを読んでいる感じではないですね。


length メソッド

で、もう1つ似ているメソッドで length がありますね。こちらも length の定義 :octocat: を見てみると

# Returns the size of the collection calling +size+ on the target.

#
# If the collection has been already loaded +length+ and +size+ are
# equivalent. If not and you are going to need the records anyway this
# method will take one less query. Otherwise +size+ is more efficient.
def length
load_target.size
end

一見 size を呼んでいるだけのように見えますが、コメントにもあるように微妙にパフォーマンスが異なります。

lengthload target をしてから size を呼ぶために必ずデータが読み込まれていることになります。

size を呼ぶと必ずしもデータをロードするわけではなく、その後の処理でデータが必要になった場合に(Rails を使う開発者が)再度ロードするだろうから、もう1クエリー増える、とのこと。

数を数える、という簡単な処理でもいろいろあるものですね。Rails は本当に奥が深いです。