`length` `size` `count`の使い分け【後編：Rails】

# はじめに
[`length` `size` `count`の使い分け【前編：Ruby】](https://qiita.com/kohki_takatama/items/c8236b6a2e267e2dad20)の続きですが、この記事だけでも完結した内容になっています。

Rubyの`length` `size` `count`を調べてる間に気づいた、「Railsだと定義が違うぞ・・・・・・」
ということで、Railsでも`length` `size` `count`について調べていきます。
※注記しない限り、`count`は引数を与えないものとします。
## この記事のまとめです
### Rails (`ActiveRecord`) の`length` `size` `count`
正確には`AcriveRecord::Associations::CollectionProxy`です。
コレクションとは、配列やハッシュなど要素の集まりのことです。

`size`: メモリ上にコレクションがあればその要素数を取得する。メモリにない場合は、件数取得SQLクエリを発行し、データベースから要素数を取得する。
→非常に効率的ですが、DBの最新状態とは異なる可能性があります。

`count`: （引数がない場合）どんな時でもSQLクエリを発行し、データベースから要素数を取得する。
→常に最新のDBに基づき、件数を取得します。

`length`: コレクションをメモリにロードし、要素数を取得する。すでにメモリ上にあれば、その要素数を取得する。
→ 全件（件数ではなく、要素そのもの）のデータを今すぐ使う必要がある場面では適していますが、単に件数だけ知りたい場合には非効率です。
### 使用シナリオ
若干正確性に欠けますが、平易な言い方で使用シナリオを示します。

まず、`count`とそれ以外では性質が違います。
`count`：（データベースの負荷を考えず）**常に最新のデータを取得したい場合**です。
`length` / `size` はケースによって最適が違います。

|  |  | 対象データの扱い<br> |  |
| ---- | ---- | ---- | ---- |
| 直前 | 直前で呼び出している | 直前で呼び出していない |  |
| 直後 | どちらでも | 直後に呼び出す | 直後に呼び出さない |
| 適したメソッド<br> | `size` / `length`（同じ）<br> | `length` | `size` |
# 本編
## それぞれのメソッドのイメージ
以下の簡単な例を用いて、それぞれのメソッドのイメージをしましょう。
```rb
pets = Pet.all # dog, cat, rabbit
pets.length # 3
pets.size # 3
pets.count # 3
```
### `length`
1. `pets`の「すべての要素`'dog', 'cat', 'rabbit'`」をデータベースからメモリに呼び出す
2. メモリ上の、呼び出した要素の数を取得する。この場合は`3`です。

いったんメモリにすべて持ってきます。
すでにメモリ上にあれば、`1`のステップをスキップして`2`のみを行います。
### `count`
1. SQLクエリを発行し、`pets`の「要素数」を取得する。つまり`3`を取得します。

要素をひとつひとつ持ってくる、なんてことはしません。
### `size`
1. メモリ上に、対象のコレクションがあるか確認する
	1. メモリ上にある場合は、`length`の`2`と同じ挙動をする
	2. メモリ上にない場合は、`count`と同じ挙動をする

どっちにしろ、`3`のみを取得します。
##  `length`はコレクションをまるまる呼び出す
`length` は、唯一 Relation を **完全にメモリにロードしてから件数を返す**メソッドです。

そのため、データ件数だけを知りたい場合には非効率ですが、
「この後すぐにそのレコードの内容を使う」のであれば、
一度ロードすれば `.length` も中身の操作も追加のDBアクセスなしに行えます。

一方、 `.size` はすでにロード済みなら配列の件数を返し、
未ロードなら `SELECT COUNT(*)` で済ませるため、件数取得としては最も効率的です。

## `count`は常に最新のデータを取得する
`count`の特徴的な点は、「唯一、常にデータベースにアクセスする」ということです。

これの何が良いのでしょうか？
それは、「常に最新のデータを取得できる」ということです。
```rb
# すべてのPetレコードを取得
pets = Pet.all

# この時点でのpetsコレクションのlengthを取得（メモリ上のデータの数）
length_before = pets.length

# 他のユーザーが新しいPet（例: 'bird'）をデータベースに追加
# Pet.create(name: 'bird')

# 再度countを使用してデータベースの現在のレコード数を取得
count_after = Pet.count

# 結果の表示
puts "Length before: #{length_before}" # 初期のメモリ上のデータ数=3
puts "Count after: #{count_after}"    # データベース上の最新のデータ数=4
```
この例は、`length`が、他のユーザーによって`bird`が追加されたことを感知できず`3`を返すこと、
`count`であれば他のユーザーの操作を感知し、`4`を返すことができることを示しています。

一方で、メモリにコレクションがある場合、常にSQLクエリを行う`count`は非効率なこともあります。

## `size` は「いいとこどり」だが、意図を明確に持つことが大切

 `size` は、すでにRelationがメモリにロードされていれば `.length` と同様に配列のサイズを返し、
 まだロードされていなければ `.count` のように `SELECT COUNT(*)` クエリを発行します。

このため、一般的には「効率的」で「柔軟に振る舞う」安全な選択肢です。

ただし以下の思い込みには注意が必要です。

- `.length` のように「コレクションを使い回せてお得」と思って `.size` を使う → **明示的に `.load` する方が安全**
- `.count` のように「常に最新のDB状態を保証」と思って `.size` を使う → **DB状態の保証はあくまで `count` 単体の話**

というメリットを得られません。この点が中途半端です。
# 終わりに
以上のことから、一概にこのメソッドが優れている、ということはありません。
ただ、個人的には`count`のメリット（常に最新のデータを取得する）は大きいと感じます。
とはいえ、処理が重くなってから変えるんじゃ遅いんだよなあ・・・・・・
使い分けは、経験を積みながら考えていこうと思います。
## この記事のまとめです ※再掲
### Rails (`ActiveRecord`) の`length` `size` `count`
正確には`AcriveRecord::Associations::CollectionProxy`です。
コレクションとは、配列やハッシュなど要素の集まりのことです。
- `length`：コレクションをメモリにロードし、要素数を取得します。
  すでにメモリ上にあれば、その要素数を取得します。
- `size`：メモリ上にコレクションがあればその要素数を取得する。
  メモリにない場合は、SQLクエリを発行し、データベースから要素数を取得する。
条件に合わせて、  `length`と`count`を使い分けるイメージです。
- `count`：どんな時でもSQLクエリを発行し、データベースから要素数を取得する。
### 使用シナリオ
若干正確性に欠けますが、平易な言い方で使用シナリオを示します。

まず、`count`とそれ以外では性質が違います。
`count`：（データベースの負荷を考えず）**常に最新のデータを取得したい場合**です。
`length` / `size` はケースによって最適が違います。

|  |  | 対象データの扱い<br> |  |
| ---- | ---- | ---- | ---- |
| 直前 | 直前で呼び出している | 直前で呼び出していない |  |
| 直後 | どちらでも | 直後に呼び出す | 直後に呼び出さない |
| 適したメソッド<br> | `size` / `length`（同じ）<br> | `length` | `size` |

# 参考
[Rails Github collection_proxy.rb](https://github.com/rails/rails/blob/9c3ffab47c3bf59320ba08e9dafdb0275cf91a5a/activerecord/lib/active_record/associations/collection_proxy.rb#L31)
[Rails API reference size](https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-size)
[Rails API reference length](https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-length)
[Rails API reference count](https://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-count)