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つの方法があるということを理解する必要があります。
- COUNT方式
- 「SELECT COUNT(*) FROM users ...」というような形のSQLを発行して、件数だけを取得する
- 件数だけが欲しいのなら一番合理的
- レコード取得方式
- 「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
- Q. 件数を取得したいタイミングで、もうcompanyに検索結果がキャッシュされているか?
以下に、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 の仲間