30
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Linkbal(リンクバル)Advent Calendar 2021

Day 23

RailsのActiveRecordのcount、length、sizeメソッドの違い

Posted at

はじめに

いよいよダットのターンです!たまにはRailsについて書かずHTMLかJavascriptばかりの記事を投稿したので今回はRailsのメソッドについて説明させていただきます。:pray:
開発者としてはこれらのメソッドを触ったことがあるはずですね。風通はこの三つの中に勝手に選んでどっちでも使って無事で計算されるので大丈夫と思いましたが、実はそうでしょうか?実際は場合によって返った結果も変わって実行する時間も異なる可能性があります。さっそくですが、始めましょう!

概要

この記事はこれらのメソッドがRubyに使われる時を気にしないでActiveRecordに使用する時だけ中心してください。結局、アソシエーションの中に幾つのレコードがあるか教えてくれますが、内部の処理がちょっと違います。
例えば、このモーデルとリレーションがあります。

app/models/post.rb
class Post
  has_many :comments, dependent: :destroy
end

app/models/comment.rb
class Post
  belongs_to :post
end

post.comments.count

SQLのCOUNTのクエリを実行して結果からエレメントを数えます。このcountの前に条件を絞ってサブセットのエレメント数を数えることもできます。

$ post.comments.count
(1.0ms)  SELECT COUNT(*) FROM `comments` WHERE `comments`.`post_id` = 1
=> 3

$ post.comments.where(author: 'dat').count
(1.0ms)  SELECT COUNT(*) FROM `comments` WHERE `comments`.`post_id` = 1 AND `comments`.`author` = `dat`
=> 1

尚、リレーションにcounter_cache: trueを使用したらcountは新しいクエリを実行せず、すぐにキャッシュの値を返るようになります。
カウンターキャッシュはこの記事に書いてありませんがこれは参考になるかと思っています。:rolling_eyes:

post.comments.length

この場合はアソシエーションのコンテンツをロードしてロードされたエレメントを数えます。このメソッドはアソシエーションが既にロードされたらもう一度クエリを実行しません。

$ post.comments.length # クエリが実行される
Comment Load (1.1ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`post_id` = 1
=> 5

$ post.comments.length # もう一度実行するとクエリが実行されなくてすぐに返す
=> 5

post.comment.size

これは以上の二つのメソッドを合わせるものです。もしコレクションが既にロードされたらlengthメソッドのようにすぐに結果を返します。逆だったらcountメソッドのようにクエリを実行して結果を返します。ザッと見るとこのメソッドが全部自動的に適当な方法を選んで実行するのでもっとも良いメソッドかなと思う人もいますね。しかし、特別なケースによって意外と間違った結果を返す場合もあります。具体的な例を出そうと思ってます。

$ posts = Post.all
Post Load (1.7ms)  SELECT `posts`.* FROM `posts`
$ posts.length
=> 3
$ posts.size
=> 3
$ post.count
(1.0ms)  SELECT COUNT(*) FROM `posts`
=> 3

以上の例を見るとコレクションが既にロードされたらcountは無駄なクエリを実行しまったとわかります。だが、ロードされたコレクションが変更したらどうなりますか。:thinking:

$ posts = Post.all
Post Load (1.7ms)  SELECT `posts`.* FROM `posts`
$ Post.last.destroy!
=> # 消されたエレメントを返す
$ posts.length
=> 3
$ posts.size
=> 3
$ post.count
(1.0ms)  SELECT COUNT(*) FROM `posts`
=> 2

つまり、新しいクエリを実行しないと変更があるかどうか分からなくて間違った結果を返してしまいました。なので、この場合はcountしか使えません。

結論(TLDR)

  • クエリから取ったデータをもう使わなかったら、countを利用すると良い
  • クエリから取ったデータを既に使ったか使う可能性があったら、lengthを利用すると良い
  • Railsに任せたかったらsizeを利用することもできる。しかし、意外と間違った結果が返される可能性があるのでよく分析してからsizeを代わりに適当なcountlengthを使用したら良い :ok_hand:

終わりに

これは自分が実装したときどっちの方がいいのか悩んで調べた内容からまとめておいたのです。具体的な説明とか他の例を見たい方は以下の記事でも参考になると思います。ここまで読んで頂いて感謝いたします。:bow:

30
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?