countじゃなくてsizeを使った方がいい!! アソシエーションがLoadされてるか調べて常に高速なメソッドを作ろう！


RailsのActiveRecordって、DBの問い合わせ回数を意識し始めると意外と悩むことって多いですよね。

少し意識している人なら、N+1問題を防ぐために`includes`, `preload`, `eagerload`メソッドなどを使っているはずです。

このエントリは、あらかじめLoadされているかで挙動が変わるメソッドを、使ったり、作ったりしたら、eagerloadもより効果的に使えて、ちょっと幸せになれるよという話です。

## 問題提起：Loadされているかいないか？何が変わるか？

まず、以下のように`post` has_many `comments` な関係があるとします。

```ruby:post.rb
class Post < ActiveRecord::Base
  has_many :comments 
end
```

ここで、あるPostのインスタンス`post`のコメント一覧をとることを考えてみましょう。

もちろん、`post.comments`で取れますが、今回は既にloadされているかどうかの挙動も含めて、見てみるために`rails c`で以下のようにコマンドを叩いてみます。

```pry(rails_console)
[2] pry(main)> post.comments.loaded?
=> false
[3] pry(main)> post.comments
  Comment Load (42.4ms)  SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 9759707]]
=> [#<Comment id: 6290480, post_id: 9759707, created_at: "2014-06-26 02:10:37", updated_at: "2014-06-26 02:10:37">]
[4] pry(main)> post.comments.loaded?
=> true
[5] pry(main)> post.comments
=> [#<Comment id: 6290480, post_id: 9759707, created_at: "2014-06-26 02:10:37", updated_at: "2014-06-26 02:10:37">]
```

ここで、注目して欲しいのが、`[3] pry(main)`の部分ではDB問い合わせが生じているけれども、2回目のクエリである`[5] pry(main)`の部分ではDB問い合わせが発生していないという部分です。

つまり、`loaded?`かどうかによって挙動をrails側が切り替えてくれています。

さて、では、コメントの数を取得したいときはどうでしょう？
もちろん、`post.comments.count`でできます。

```pry(rails_console)
[6] pry(main)> post.comments.count
   (0.8ms)  SELECT COUNT(*) FROM "comments" WHERE "comments"."post_id" = $1  [["post_id", 9759707]]
=> 1
```

注目点は、`post.comments.count`だと新しい`COUNT`を使ったSQLクエリが発生している部分です。

これに対し、`to_a`してから数えてあげると、以下のようにDB問い合わせが発生しません。

```pry(rails_console)
[7] pry(main)> post.comments.to_a.count
=> 1
```

これは、`post.comments`が既にロードされているので、`Array#count`
を呼んで配列のサイズを返すことで、`to_a`することが結果として高速な問い合わせになっているわけです。

ちなみに`[6] pry(main)`の時は、[ActiveRecord::Associations::CollectionProxy](http://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html)の`count`メソッドが呼ばれています。


## 実際コードに書くときはどちらを使うべきか？

さて、ということはコメントの数を数えるときは`post.comments.to_a.count`の方がいいのか！と言うとそういうわけではありません。

SQL的には`SELECT *`より`SELECT COUNT(*)`の方が速いので、`post.comments`が既にロードされていない時はこの`to_a`を使った方が遅くなってしまいます。

じゃぁどうするのかといえば、既にロードされているか調べて条件分岐すれば良いわけですね。

つまり、`loaded?`メソッドを使って、


```ruby:post.rb
class Post < ActiveRecord::Base
  has_many :comments

  def comment_count
    if comments.loaded?
      comments.to_a.count
    else
      comments.count
    end
  end
end
```

というようなのを作って、

```
post.comment_count
```

とやれば、良いとこどりをできます。

これが、真価を発揮するのは更にネストした時で、例えば、

```
user.posts.map(&:comment_count)
```

とやった時に、`User.incldues(post: :comments)`と事前にやっているか否かでちゃんと挙動を変えてくれます。
素晴らしい。

### もっと良い方法はないのか？


ここまで読んで、まてまて、カウントを取るだけでいちいちメソッドを作るんじゃなくて、`post.comments.count`の`count`自体がそういう判断をしてくれればいいんじゃないの？と思う人がいるかも知れません。

まったくもってその通りで、実はそれがActiveRecordの[size](http://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-size)メソッドです。
実はこういった細かい部分で[count](http://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-count)メソッドと違うわけです。

### ちなみに：`loaded?`の使い方

[loaded?](http://api.rubyonrails.org/classes/ActiveRecord/Associations/CollectionProxy.html#method-i-loaded-3F)をどういうふうに使うかは多少難しく、具体的には`has_one`, `belongs_to`の場合は、そのまま使うことができません。

例えば、commentが所属しているpostがLoadされているか調べるとき、

```
comment.post.loaded?
```

は、`comment.post`の時点で、クラスがPostクラスになってしまうため、エラーが出ます。

この場合、

```
comment.association(:post).loaded?
```

と、associationメソッドを使って明示的にAssociationにするとうまくいきます。

このassociationメソッドは`has_many`に対しても問題なく動くので、associationメソッドを常に使うようにするという方針もありかもしれないです。


## まとめ

- ActiveRecordでは特に理由がなければ`count`ではなく`size`を使いましょう！
- `loaded?`で条件分岐することで、自分で作った少し複雑なDB問い合わせを含むメソッドも、常に最適なクエリを発行することが可能です。積極的に取り入れましょう！


(注) これはRails4の内容で、Rails3の頃は少し違った実装でした。ただ、loadedの概念は以前からあったので同じことはできるはずです。