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

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

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

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

```rb
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で利用するのに、次の２つの方法があるということを理解する必要があります。

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

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

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

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

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

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

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

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

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

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

```rb
  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. すでにレコード詳細情報を取得済みの状態かどうか、という２つの条件によって、適するメソッドを選べば良いということなのです。毎回、調べて判断するのは煩わしいと思いますので、ぜひ、以下のような短いイメージで覚えてしまいましょう。

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

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