Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
158
Help us understand the problem. What is going on with this article?
@awakia

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

More than 5 years have passed since last update.

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

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

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

問題提起:Loadされているかいないか?何が変わるか?

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

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

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

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

[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でできます。

[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問い合わせが発生しません。

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

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

ちなみに[6] pry(main)の時は、ActiveRecord::Associations::CollectionProxycountメソッドが呼ばれています。

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

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

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

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

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

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.countcount自体がそういう判断をしてくれればいいんじゃないの?と思う人がいるかも知れません。

まったくもってその通りで、実はそれがActiveRecordのsizeメソッドです。
実はこういった細かい部分でcountメソッドと違うわけです。

ちなみに:loaded?の使い方

loaded?をどういうふうに使うかは多少難しく、具体的には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の概念は以前からあったので同じことはできるはずです。

158
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
awakia
検索とか推薦とかやってきたエンジニア。早稲田の山名研出身。大学院の頃、論文を書こうとしない僕を見かねた教授に、北京のMSRAに追放されるが3ヶ月後無事帰還。 大学を卒業後、エンジニアのブラックホールとの別名を持つGoogleに吸収されそうになるが1年2ヶ月後無事生還。 現在は、Wantedly(https://www.wantedly.com/ )の4番目のエージェントとして救出活動に専念。
wantedly
「シゴトでココロオドル」ためのビジネスSNS「Wantedly」の開発・運営をしています。

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
158
Help us understand the problem. What is going on with this article?