160
143

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 5 years have passed since last update.

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

Last updated at Posted at 2016-02-12

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

基本の3メソッド

  • count - 単発 = 呼ばれる都度 COUNT SQLを発行
  • size - 日和見 = キャッシュがあればキャッシュを数える。キャッシュがなければcount
  • length - 蓄える = キャッシュがなければキャッシュして数える

その他のメソッド

  • empty?, any? - size の仲間
  • exists? - count の仲間
  • blank?, present?, none? - length の仲間
160
143
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
160
143

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?