LoginSignup
47
34

More than 5 years have passed since last update.

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

Last updated at Posted at 2015-03-04

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 は本当に奥が深いです。

47
34
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
47
34