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

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

今回はこんなやつを実装しました。
<img width="1028" alt="スクリーンショット 2019-12-21 12.36.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/ed12a867-87b0-ac81-a03d-ff5091acabfd.png">

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

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

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

```ruby: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側で呼び出します。

```haml: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](https://i.gyazo.com/13e1dd4bd0188cdf9fe6b54e5ff88082.gif)](https://gyazo.com/13e1dd4bd0188cdf9fe6b54e5ff88082)

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

## 変化球
前後レコードではなく、DB内のデータをランダムで取得するあれを実装を試みてみます。
ネットサーフィンしながらいくつか方法を見つけたので試してみます。
### 1. RAND()関数 
MySQLのネイティブ関数RAND()を使用してみます。

```ruby:items_controller.rb
def show
  #......省略

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

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

```haml: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](https://i.gyazo.com/2e28da12590b4c3c01b815f1731202aa.gif)](https://gyazo.com/2e28da12590b4c3c01b815f1731202aa)
<img width="965" alt="スクリーンショット 2019-12-21 14.32.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/f2ac1c7a-bda3-c133-c725-1871705c9650.png">
......ねこもうさぎも被りました。

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

別の方法を試します。

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

```ruby:items_controller.rb
def show
  #......省略

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

　#......省略
end
```
```haml: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](https://i.gyazo.com/03c05ebc289c36c395e5926dec5e7bef.gif)](https://gyazo.com/03c05ebc289c36c395e5926dec5e7bef)
で！！！け！！！た！！！
random1とrandom2で別々の値が取ってこれてます。さすがゴリ押し。
これにて実装完了......と思ったその時。見つけてしまいました。

[ActiveRecord でランダムなレコードが欲しい(1件、複数件)](https://blog.44uk.net/2012/12/22/random-record-by-activerecord/)

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

.........ダサい？？？

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

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

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

```ruby:items_controller.rb
def show
  #......省略

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

　#......省略
end
```
```haml: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](https://i.gyazo.com/10e6cd641710ca27b986b99d5d39491b.gif)](https://gyazo.com/10e6cd641710ca27b986b99d5d39491b)
......ランダムなってるやん。
さすがスマート。

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

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

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

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

[Rails: データベースのパフォーマンスを損なう3つの書き方（翻訳)](https://techracho.bpsinc.jp/hachi8833/2018_01_11/50793)

```ruby: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
```
```haml: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](https://i.gyazo.com/d7fdeb36af1c9af6e5a4ff611e7f1a6e.gif)](https://gyazo.com/d7fdeb36af1c9af6e5a4ff611e7f1a6e)

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

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

1個目
<img width="1522" alt="スクリーンショット 2019-12-21 16.06.59.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/44ca5fed-f730-d4e0-e28e-12bdca3eb704.png">
2個目
<img width="396" alt="スクリーンショット 2019-12-21 16.06.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/d57c8af5-838a-5dac-dd4d-c86b341c9852.png">
3個目
<img width="542" alt="スクリーンショット 2019-12-21 16.07.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/b03c9c04-bd3e-7feb-9843-863551665404.png">
4個目
<img width="685" alt="スクリーンショット 2019-12-21 16.08.35.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/1fec085f-54f3-4fe0-853f-5b7a0d3f213e.png">

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

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

```ruby: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
```
<img width="1463" alt="スクリーンショット 2019-12-21 22.39.10.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/69da9a71-fc67-25af-d180-b8b2bbc639f4.png">
もう一度検索スピードを検証してみます。

1個目
<img width="1430" alt="スクリーンショット 2019-12-21 22.41.53.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/06a71bad-f637-8898-98af-e60d4c6a71e4.png">
2個目
<img width="407" alt="スクリーンショット 2019-12-21 22.43.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/e3f87c39-fcfb-205e-0a04-2cff7c5dde28.png">
3個目
<img width="555" alt="スクリーンショット 2019-12-21 22.45.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/86b96d4d-ddb0-eed0-1679-7f24625189c8.png">
4個目
<img width="689" alt="スクリーンショット 2019-12-21 22.46.11.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/493565/717ce252-c1cc-4e60-7386-c33eb084ded1.png">

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

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

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

おわり。