質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

90.03%

データをランダムに複数件取得する際の(たぶんID落ちによる)エラーへの対処

解決済

回答 1

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 733

Gr.

score 49

 複数データのランダム取得

以下のコードでランダムに4冊の本のデータを取得しています。

def sample
  @books = Book.offset(rand(Book.count)).first(4)
end


取得した4冊の本のタイトルをviewで出します。

<ol>
  <% @books.each do |book| %>
  <li>
    <%= book.title %>
  </li>
  <% end %>
</ol>


ここまでは問題なく表示できます。
表示例)

  1. 罪と罰
  2. こころ
  3. 地獄変
  4. 斜陽

リロードすると…

  1. 地獄変
  2. 砂の器
  3. 3匹のおっさん
  4. 吾輩は猫である

 (たぶんID落ちによる)エラー

上記のviewではeachを使って取得したデータ全て(4冊)を表示しました。
今度は取得したデータそれぞれについて個別に表示させようと思い、以下のように追記しました。

<ol>
  <% @books.each do |book| %>
  <li>
    <%= book.title %>
  </li>
  <% end %>
</ol>

   ↓ 以下を追記
<%= @books.first.title %>
<%= @books.second.title %>
<%= @books.third.title %>
<%= @books.fourth.title %>


すると、基本的には問題なく表示できるのですが、リロードした際、時折エラーが起きるようになりました。エラー内容は以下のものです。

undefined method `title' for nil:NilClass

このエラー文に該当する箇所は、先ほど追記したうちの <%= @books.second.title %>か <%= @books.third.title %>か <%= @books.fourth.title %>です。 secondで引っ掛かったり、fourthで引っ掛かったり、その時々です。

エラーになる場合、おそらくcontrollerがデータを集める時点で空のIDを含んでいるのだと思いました。

例)Bookデータのidが1~10あり、そのうち5,6,7が削除済みだったとする。 controllerでランダムに取得した4つのデータのidが1,4,6,9だった場合、削除済みのid=6はnilなので <%= @books.third.title %> でエラーとなる。これより上記のeach文は、取得できた1,4,6,9のうちtitleのある1,4,9のみ表示している(エラーなので確認できないが)。

こんなことだろうな、これはIDに抜けがないときだけ使おう、と思っていたのですが…

いや待てよ、たしか… エラーが起きたのは <%= @books.second.title %> などを追記してからだから、 追記部分を削除してeach文だけ残すと…

あれ? 不思議とエラーは起こらず毎回必ず4件のtitleがランダムに取得できます。

もうわかりません。

  • ※Bookデータは計20件入っています。
  • ※20件のうちいくつか削除したデータがあります。
  • ※リロードすると4~5回に1回のペースで上記エラーが起こります。
  • ※追記した箇所を削れば(each文だけでは)エラーは起きません。
  • ※追記があっても、each文の箇所ではエラーは起きません。
  • ※<%= @whgs.first.title %>でエラーになったことはありません。

 教えていただきたいこと

  1. エラーの起きる原因と、なぜeach文だけではエラーにならず毎回4件取れるのか。
  2. エラーの起きる原因がIDが欠けていることだった場合、欠けたIDをうまく避けて4件のレコードを取得するには、controller側にどう書けばいいか。

controllerの記述を以下のものに変えてみても結果は全く同じでした。

def sample
  @books = Book.where('id >= ?', rand(Book.first.id..Book.last.id)).first(4)
end

プログラミング初心者です。よろしくお願いいたします。

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 1

checkベストアンサー

+1

エッジケースを満たせていませんね。
Book.count == 20のときにrand(20)が19に評価された場合

Book.offset(19).first(4)

となり、最後の1冊しか取得できませんよね。

4 / (0..19).size = 1/5となり、現象が4〜5回に1回起きるのも順当でしょう。

batch_size = 4
Book.offset(rand(Book.count-batch_size)).first(batch_size)

などとするのが妥当ではないでしょうか。
しかしながら、Book.countがそもそも4未満のときも考えなければならないです。

(あと「ランダムにn件取得」は、厳密にはLIMIT, OFFSETでは実現できませんね。Railsでは何もしなければ自動でidによるsortが入る様になっているので、連続したレコードしか取得できないですし。)

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2018/09/20 10:49

    takumiabeさん
    回答ありがとうございます!
    batch_sizeの記述でエラーはなくなりました。
    batch_size、初見でした…。

    取得した4件のidを見てみたところ、確かに連番になっており、ご指摘の通り厳密には「ランダムにn件取得」は果たせていませんでした(データが少なく、かつeach文の並びをshuffleしていたため気づきませんでした)。

    では、自動でidによるsortが入る様になっているRailsで「ランダムにn件取得」するためにはどういった処理が有効でしょうか。
    もしご存知でしたらヒントだけでもいいのでお与えください。

    キャンセル

  • 2018/09/24 01:03

    動作の保証はしませんが
    https://qiita.com/yancya/items/b963c3c0b3e3de646b6b
    などが参考になるのではないでしょうか。

    キャンセル

  • 2018/09/24 14:53

    takumiabeさん
    返答していただきありがとうございます!
    色々調べていくうちに.sampleという書き方を見つけました。
    ランダムに(n個)を取ってこれるようで、
    @books = Book.sample(4)
    これだけで十分でした。

    回答をいただけたおかげで、railsによって自動でidによるsortがかかる仕様を知れたので、それをヒントに調べて見つけられました。
    たいへん助かりました!ありがとうございました!

    キャンセル

  • 2018/09/25 14:04

    解決して良かったです〜

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 90.03%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

同じタグがついた質問を見る