2
3

More than 3 years have passed since last update.

[Rails]特定のインスタンスに対して前後のレコードを取得したりランダムで取得したりするあれを試してみた

Posted at

はじめに

こんにちはどうも、pirikaraです。
髪の毛を切りました。

今回はこんなやつを実装しました。
スクリーンショット 2019-12-21 12.36.00.png

特定の投稿(今回はitemの出品)に関して、DBから前後のレコードを取得して表示させたりリンクを飛ばす感じのあれです。
あとDBからランダムにレコードを取得して表示させたりリンク飛ばしたりする感じのあれです。

まずは特定の投稿に関してDBから前後レコードを取得する奴から実装していきます。
Rails標準のAPIでは見つからなかったので、今回はmodelに対してメソッドを書き込んでいきます。

いざ、実装

今回はItemクラスのインスタンスに関して、その前後レコードを取得するメソッドをmodels/item.rbに記述していきます。
前のレコードを取得するメソッドを『previous』
後のレコードを取得するメソッドを『next』
としてmodelに定義していきます。

item.rb
class Item < ApplicationRecord
  #......省略


  def previous
    Item.where("id < ?", self.id).order("id DESC").first
  end

  def next
    Item.where("id > ?", self.id).order("id ASC").first
  end
end

現在のインスタンス(self)のidより大きいか小さいかで前後レコードを判定・取得します。
作成したメソッドをview側で呼び出します。

items/show.haml
#......省略

- if @item.previous.present?
  =link_to item_path(@item.previous.id) do
    = @item.previous.name
- else
- else
  .none
- if @item.next.present?
  =link_to item_path(@item.next.id) do
    = @item.next.name
- else
  .none
#......省略

controller側で@item = Item.find(params[:id])などとし、Itemのshow画面に遷移。
@itemに対して『previous』 『next』メソッドを使用することで、@itemの前後レコードを取得できます。

また、前後レコードがない場合(最新のレコード、もしくは最古のレコードの場合)にリンクを表示しないよう『present?』メソッドによって真偽判定、条件分けしています。

Image from Gyazo

左右感がちょっと気持ち悪いですが、実装できました。(あとで直します。)

変化球

前後レコードではなく、DB内のデータをランダムで取得するあれを実装を試みてみます。
ネットサーフィンしながらいくつか方法を見つけたので試してみます。

1. RAND()関数

MySQLのネイティブ関数RAND()を使用してみます。

items_controller.rb
def show
  #......省略

  @random1 = Item.order("RAND()").limit(1)
  @random2 = Item.order("RAND()").limit(1)

 #......省略
end

左右のリンクがあるので、それぞれ@random1@random2として変数をインスタンス変数を定義してみました。
viewはこちら

items/show.haml
- if @random1.present?
  =link_to item_path(@random1.ids) do
    - @random1.each do |random1|
      = random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.ids) do
    - @random2.each do |random2|
      = random2.name
- else
  .none

RAND()関数で取得したデータにlimitつけてましたが、配列で取得されるみたいなので『ids』としないとid取れませんでした。
また、nameについてもeach文で取り出してあげないと表示されませんでした悲しい。

そして結果がこちら。
Image from Gyazo
スクリーンショット 2019-12-21 14.32.36.png
......ねこもうさぎも被りました。

このあと何回もページ遷移してみました。
確かにランダムで表示されるようにはなったようですが、左右の値が毎回同じでした。
実際にDBテーブル内に存在する値からランダムに取得してくれるのはありがたいですが、
randam1とrandam2で別々に値を取得してくれるなんてそんな都合よく世界はできていなかったようです。

別の方法を試します。

2. Model.all.sample

モデルから全てのレコードを取得した上で1件抽出する感じ。
ゴリ押ししてみます。

items_controller.rb
def show
  #......省略

  @random1 = Item.all.sample
  @random2 = Item.all.sample

 #......省略
end
items/show.haml
- if @random1.present?
  =link_to item_path(@random1.id) do
    = @random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.id) do
    = @random2.name
- else
  .none

今回は配列でなくデータ単体を取ってこれたので、idsとかeach文なんてまどろっこしいことをせずに済みました。やったね。
気になる結果は......
Image from Gyazo
で!!!け!!!た!!!
random1とrandom2で別々の値が取ってこれてます。さすがゴリ押し。
これにて実装完了......と思ったその時。見つけてしまいました。

ActiveRecord でランダムなレコードが欲しい(1件、複数件)

『全件取ってランダムに1件取り出すのはダサい』

.........ダサい???

確かに全部とってランダムに1件はメモリやらなんやらに理解の浅い僕でも効率が悪いのは理解できます。
こういう時にindexが便利なのか......?

とにかくダサいのは嫌なので別の方法を試します。(終わりが見えない)

3. Model.offset(rand(Model.count)).limit(1)

記事の方法をパクり...参考にさせていただきました。ありがとうございます。
レコード未満のランダム生成された整数をoffsetでレコード取得の開始位置とし、1個だけデータを取ってくる感じです。

items_controller.rb
def show
  #......省略

  @random1 = Item.offset( rand(Item.count) ).limit(1)
  @random2 = Item.offset( rand(Item.count) ).limit(1)

 #......省略
end
items/show.haml
- if @random1.present?
  =link_to item_path(@random1.ids) do
    - @random1.each do |random1|
      = random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.ids) do
    - @random2.each do |random2|
      = random2.name
- else
  .none

......each文でないと取り出せませんでした......idsも復活しました......
しかしoffsetで開始を指定している分、Model.all.sampleの時よりはスマートなデータ取得ができている気がします。
さて結果は......
Image from Gyazo
......ランダムなってるやん。
さすがスマート。

『自分のデータは除いて......』の実装はしていないのでGIFでは可愛いうさぎが延々と出現してしまっていますが、
きちんとランダムにリンクが生成されています。

これで終わりにしようと思いましたが、僕は根に持つタイプなのでダサくない方法をもう一つ考えてみます。

4. Model.where('id >= ?', rand(Model.first.id..Model.last.id)).limit(1)

offset、 limitを使った書き方の書き換えとしてwhereに置き換える方法が紹介されていたので、試してみます。
randの引数でランダム数値生成の範囲指定を行いますが、今回は先頭レコードと最終レコードのidを範囲として指定しています。

Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)

items_controller.rb
def show
  #......省略

  @random1 = Item.where('id >= ?', rand(Item.first.id..Item.last.id)).limit(1)
  @random2 = Item.where('id >= ?', rand(Item.first.id..Item.last.id)).limit(1)

 #......省略
end
items/show.haml
- if @random1.present?
  =link_to item_path(@random1.ids) do
    - @random1.each do |random1|
      = random1.name
- else
  .none
- if @random2.present?
  =link_to item_path(@random2.ids) do
    - @random2.each do |random2|
      = random2.name
- else
  .none

こちらもeach文、idsを用いて配列からデータを取り出して出力しています。
結果は......
Image from Gyazo

ランダムになってました。満足。

比較

rails consoleからSQL文とSQL発行の所要時間が確認できるので、
上記4つのものを確認・比較してみます。

1個目
スクリーンショット 2019-12-21 16.06.59.png
2個目
スクリーンショット 2019-12-21 16.06.08.png
3個目
スクリーンショット 2019-12-21 16.07.41.png
4個目
スクリーンショット 2019-12-21 16.08.35.png

......DBにデータが7つしかないので速さは大差ないですね。そりゃそうか。
SQL文は4個目では3回発行されています。あまりよろしくないですね......
スマートな実装としては3個目推奨でしょうか。

という訳でseeds.rbを読み込んでデータを1000件作ってみました。

seeds.rb
1000.times do |index|
  Item.create!(name: "アイテム#{index}",
              seller_id: 1, 
              description: "内容#{index}", 
              category_id: index,
              condition_id: index, 
              prefecture_id: index, 
              sendingmethod_id: index, 
              postageburden_id: index, 
              shippingday_id: index, price: index, 
              profit: index)
end

スクリーンショット 2019-12-21 22.39.10.png
もう一度検索スピードを検証してみます。

1個目
スクリーンショット 2019-12-21 22.41.53.png
2個目
スクリーンショット 2019-12-21 22.43.45.png
3個目
スクリーンショット 2019-12-21 22.45.02.png
4個目
スクリーンショット 2019-12-21 22.46.11.png

SQL発行の所要時間に大きく差が出ました。
1個目、2個目は3個目、4個目に比べて10倍以上の時間がかかってしまっています。
クエリ文が3回発行されてしまっていることを考えると、
今回試した手法の中ではoffsetを用いた3番目の方法がベストプラクティスとなります。

おわりに

今回はSQLについて理解を深めることができました。
Indexを用いた検索でも検証を行ってみたいですね。
大規模なWebアプリケーションになればなるほど『どんな手法で検索をするか』で処理スピードに大きな差が生まれてしまうことを実感できました。

また気が向いたらなんか書きます。

おわり。

2
3
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
2
3