Ruby on Rails の Active Record
でいつも何気なく size
を使っていたら、なぜかデータベースにエントリがあるはずなのに正しく数を数えられていない症状が起きました。
もう結構出ている話なのですが、size
って一度ロードしたデータをロードし直してくれないんですね。
せっかくなのでコードレベルで詳しく調べてみました。
size
メソッド
まずは size
の定義 から。
# 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?
だと既にロードしてある target
の size
を返すようです。
ちなみに loaded?
はこのクラスの親クラスである Association
で定義されており、シンプルにインスタンス変数 @loaded
の値を返します。
count
メソッド
次に count メソッド の定義です。
# 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
の定義 を見てみると
# 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
を呼んでいるだけのように見えますが、コメントにもあるように微妙にパフォーマンスが異なります。
length
は load target
をしてから size
を呼ぶために必ずデータが読み込まれていることになります。
size
を呼ぶと必ずしもデータをロードするわけではなく、その後の処理でデータが必要になった場合に(Rails を使う開発者が)再度ロードするだろうから、もう1クエリー増える、とのこと。
数を数える、という簡単な処理でもいろいろあるものですね。Rails は本当に奥が深いです。