ActiveRecord の has_many関連、件数を調べるメソッドはどれを使えばいい?


ActiveRecord で、has_many で定義した関連があるとき、その関連の件数を取得するのには、count, size, length の3つのメソッドがあります。さらに、0件かどうかを調べるためには、empty? や exists? といった問い合わせメソッドもあります。

これらの使い分けについて、なるべく分かりやすくなるように解説してみたいと思います。

はじめに、has_many関連とは、次のようなコード例における、company.users のことを指します。

class Company < ActiveRecord::Base
  has_many :users
end

class CompanyUsersController < ApplictaionController
  def index
    company = Company.find(params[:company_id])

    # 件数が1件以上なら何かをしたい
    if company.users.count > 0 # 注目!ここが色々な書き方ができます
      ...
    end
  end
end

上記のコード例の

company.users.count > 0

の部分は、ほかにも、以下のように書くことができます。

  • company.users.size > 0
  • company.users.length > 0
  • !company.users.empty?
  • company.users.present?
  • !company.users.exists?
  • !company.users.any?
  • company.users.none?

…沢山ありますね!

これらのメソッドは、どれを使うのが一番良いのでしょうか?

主な選択基準は、レコードの取得に関する「効率」

has_many 関連の件数取得のメソッドとしてどれを使うのが一番良いかは、状況によって違います。「とりあえずこれを使っておけば安心」というようなものは決められません。ただし、もしもどれが最適かをまったく判断できないのであれば、size を使っておくのが、比較的問題が起きづらいでしょう。(以降の説明を読んで、判断できるようになれば幸いです。)

「効率」の違い

company.users のような has_many 関連を Rails で利用して、件数を入手したい場面において、利用するメソッドで、効率はどのように違ってくるのでしょうか。

これを理解するには、RDB(リレーショナルデータベース)からSQLで件数を取得してRubyで利用するのに、次の2つの方法があるということを理解する必要があります。

  1. COUNT方式
    • 「SELECT COUNT(*) FROM users ...」というような形のSQLを発行して、件数だけを取得する
    • 件数だけが欲しいのなら一番合理的
  2. レコード取得方式
    • 「SELECT * FROM users ...」というような形のSQLを発行して、結果をすべて取得する
    • その上で、取得したレコード数を数える
    • レコードの詳細と件数の両方が欲しいなら一番合理的

一般的に、発行するSQLの回数は少ないほうがよく、取得する情報の量も(ニーズを満たす限りは)少ないほうが省メモリでメリットがあります。

そのため、もしも、company.users の件数が1回入手できれば満足なのであれば、1のアプローチが合理的です。この場合に2を使ってしまうと、本来不要なレコードの取得をしてしまい、効率が悪くなるのです。

しかし、結果の詳細(1つめのuserレコードでは氏名が○○さんで、誕生日がいついつで、というような情報)も件数もどちらも取得したいのなら、2のアプローチが合理的です。1のアプローチを使うと、件数取得とレコード取得で2回SQLを走らせなければならなくなるからです。

has_many 関連は、一度レコードを検索すると、レコードを内部にキャッシュする

先に進むまえに、もう一つ理解しておくべき前提があります。has_many 関連は、前述の2の方式で内部的にレコードを取得すると、その結果をキャッシュ(メモリ上に保管)しておきます。

これはいつ起きるのでしょうか? レコードが取得されるのは、"本当に必要になったとき" です。以下に、内部的なレコード取得が起きる例と、起きない例を挙げておきます。

内部的にレコード取得が起きない例

  company = Company.find(params[:company_id])
  @users = company.users # ここではレコード詳細は実際にはまだ要らないのでSQLはまだ発行されていない

内部的にレコード取得が起きる例

  company = Company.find(params[:company_id])
  company.users.each do |user| # ここではレコード取得が走り、内部にデータがキャッシュされます
    # 1件ずつなにかの処理をする
    ...
  end
  @users = company.users # ここでは、すでにキャッシュされているので、レコード取得は新しく走りませんが、内部的に結果はキャッシュされたままです

どのメソッドがどの方式で件数を求めるか

いよいよ本題です。

どのメソッドをつかってhas_many関連の件数を取得するのが良いのでしょうか。それは、件数を取得する際の「状況」によって変わります。以下が、現在の状況によってどれをつかうべきかの大まかな判断フローです。(ほかのメソッドでも同じ役目をすることがありますが、わかりやすくするために特徴的なメソッドを挙げています)

  • Q. 件数だけが欲しいのか、それとも、レコードの詳細も欲しいのか?
    • A. 件数だけが欲しい。レコードは(companyオブジェクトを利用するシーン全体で)要らない → COUNT方式 → count
    • A. レコードの詳細も欲しい
      • Q. 件数を取得したいタイミングで、もうcompanyに検索結果がキャッシュされているか?
        • A. キャッシュされている → size
        • A. キャッシュされていない → length

以下に、count, size, length, empty?, exists?, any? がどのような動きをするかを、レコード取得との関係に着目して表します。

メソッド 動き 適する状況 適さない状況
count 前述のCOUNT方式のSQLを発行して結果を返す 1回だけ件数だけが欲しい。レコード取得は不要。もしくは、キャッシュされたデータでは古過ぎるかもしれなくて、現在ただいまの件数を特に知りたい場合。 すでにレコード取得をしていたり、あとで確実にレコード取得をする予定がある場合。もしくは、欲しいのは件数だが何回も count メソッドを呼んでいる場合(呼ぶたびにSQLが発行されるので、結果を変数にとって使いましょう)。
size レコード取得を行った後なのであれば、SQLを発行せず、キャッシュされたデータの件数を返す。レコード取得を行う前なのであれば、COUNT方式のSQLを発行して結果を返す。※0件の場合は空だという検索結果もキャッシュされます。 すでにレコード取得を行ったあとである場合 まだレコード取得前であり、あとでレコード取得が発生することがわかっている場合。
length レコード取得を行っていなければ行った上で、件数を返す 同じ文脈でレコード詳細も必要になるのがわかっている場合。特に、後からレコード取得が発生することがわかっている場合。 レコード取得が必要ない場合。
empty? size と同様 size と同様 size と同様
exists? 内部的には件数取得でなく「1件のレコードを最小限の情報で得る」アプローチで1回SQLを発行する count と同様 count と同様
any? size と同様 size と同様 size と同様
blank?, present?, none? レコード取得を行ってから動作する レコード取得をしたいのであれば可 レコード取得が不要である場合

まとめ

いかがでしょうか。基本的には、1. 自分がレコード詳細情報も欲しいのかどうか、2. すでにレコード詳細情報を取得済みの状態かどうか、という2つの条件によって、適するメソッドを選べば良いということなのです。毎回、調べて判断するのは煩わしいと思いますので、ぜひ、以下のような短いイメージで覚えてしまいましょう。

基本の3メソッド
* count - 単発 = 呼ばれる都度 COUNT SQLを発行
* size - 日和見 = キャッシュがあればキャッシュを数える。キャッシュがなければcount
* length - 蓄える = キャッシュがなければキャッシュして数える

その他のメソッド
* empty?, any? - size の仲間
* exists? - count の仲間
* blank?, present?, none? - length の仲間