rails7にsotalbeを実装

# はじめに
久々にrailsの並び替え機能を実装したので、その際の作業録です。

# 実装
昔は、以下のgemを利用した時期もありましたが、jqueryが利用されていることだったり、メンテがされてないとかがあったので、今回は利用しないことにしました
https://github.com/itmammoth/rails_sortable

今回は、stimuls componetsにあるこちらを利用しました。
https://www.stimulus-components.com/docs/stimulus-sortable

### javascript関係
```
yarn add @stimulus-components/sortable sortablejs @rails/request.js
```

```js:app/javascript/controllers/index.js
import { Application } from '@hotwired/stimulus'
import Sortable from '@stimulus-components/sortable'

const application = Application.start()
application.register('sortable', Sortable)
```

### view 関係
今回テーブル内を入れ替える予定なので、tbody配下全てを対象とするため以下のようにしました。
```haml:index.html.haml
      %tbody{ 'data-controller': 'sortable', 'data-sortable-handle-value': '.handle'}
        = render partial: 'test', collection: @tests
```

並びが終わった直後のポスト先を指定しときます
```haml:_test.html.haml
%tr{ 'data-sortable-update-url': position_change_admin_test_path(test)}
  %td.border.cursor-pointer{class: 'handle'}
    %i.fa-solid.fa-up-down
  %td.border
  %#- 省略
```

```ruby:routes.rb
    resources :tests, shallow: true do
      patch :position_change, on: :member
    end
```

ここまで設定していると、とりあえず動く状態にはなるかと思います
![l_6814799_124_8b0ce0244265a8ffadc1a11eebd167c2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3632357/396c2c01-fa82-0b09-c60e-4def5d807a32.png)

コントローラーにはこんな感じで、パラメータが送られてくるのが確認できました。
```
 Parameters: {"position"=>"11", "id"=>"1"}
```


### コントローラー 関係
少し力業が否めないですが、自分はこのように実装しました。

#### 考え方：①クエリ上で表示順が欲しい
データが取得した後に番号を割り当てるのもできますが、クエリ内でそれを実装したかったので、以下のようなクエリを生成していました。

```ruby:controller
ad_num_query = Test.all.select('id , ROW_NUMBER() OVER(ORDER BY tests.display_order ASC) as row_num')
Test.all.joins("JOIN (#{ad_num_query.to_sql}) sub ON tests.id = sub.id")
```
クエリはこんな感じになります
```sql
SELECT
    `tests`.* 
FROM
    `tests` 
    JOIN ( 
        SELECT
            id
            , ROW_NUMBER() OVER (ORDER BY tests.display_order ASC) as row_num 
        FROM
            `tests` 
    ) sub 
        ON tests.id = sub.id 
```

#### 考え方：②①の順番でレコードを更新したい
既存レコードが１から順次割り当てられてるとは限らないので、割り当てる処理を考えてみました。
```ruby:admin::testController
ad_num_query = Test.all.select('id , ROW_NUMBER() OVER(ORDER BY tests.display_order ASC) as row_num')
Test.all.joins("JOIN (#{ad_num_query.to_sql}) sub ON tests.id = sub.id")
    .update_all('display_order = sub.row_num')
```
クエリはこんな感じになります
```sql
UPDATE `tests` 
    JOIN ( 
        SELECT
            id
            , ROW_NUMBER() OVER (ORDER BY tests.display_order ASC) as row_num 
        FROM
            `tests` 
    ) sub 
        ON tests.id = sub.id 
SET
    display_order = sub.row_num 
```

#### scopeを準備
上記で毎回同じjoinを利用してたので、scope化しました
```ruby:model
  scope :add_row_num, -> {
    ad_num_query = self.select('id , ROW_NUMBER() OVER(ORDER BY tests.display_order ASC) as row_num')
    joins("JOIN (#{ad_num_query.to_sql}) sub ON tests.id = sub.id")
  }
```

#### ①と②を踏まえ、コントローラーを作成
```ruby:コントローラー
def position_change
  @test = Test.find(params[:id])
  return unless @test.update(display_order: params['position'])

  base_query = Test.all
  if params['position'].to_i == 1
    # p '最初'
    base_query.where.not(id: @test).add_row_num
              .update_all('display_order = sub.row_num + 1')
  elsif params['position'].to_i == base_query.size
    # p '最後'
    base_query.where.not(id: @test).add_row_num
              .update_all('display_order = sub.row_num')
  else
    #  p '途中'
    base_query.where.not(id: @test).add_row_num
              .where(display_order: ..params['position'].to_i)
              .update_all('display_order = sub.row_num')

    base_query.where.not(id: @test).add_row_num
              .where(display_order: params['position'].to_i + 1..)
              .update_all('display_order = sub.row_num + 1')
  end
end
```

**※補足説明**
<dl>
  <dt>【最初(position=1)】</dt>
  <dd>
    ・自分のレコードは先にposition=1で更新
    <br/>
    ・自分を除くレコード(この時点では、1からスタートされてる)をposition=row_num + 1で更新
  </dd>
  <dt>【最後(position=last)】</dt>
  <dd>
    ・自分のレコードは先にposition=lastで更新
    <br/>
    ・自分を除くレコードをposition=row_numで更新
  </dd>
  <dt>【途中(position=???)】</dt>
  <dd>
    ・自分のレコードは先にposition=lastで更新
    <br/>
    ・自分を除く自分より数字の小さいレコードをposition=row_numで更新
    <br/>
    ・自分を除く自分より数字の大きいレコード(この時点では、配置したpositionからスタートされてる)をposition=row_num + 1で更新
    <br/>
  </dd>
</dl>

# さいごに
コントローラーに関しては、色々なやり方があると思います。
自分のやり方も正解とは思っていませんので、あくまで参考レベルで見ていただけると幸いです。
もっといい方法あるよ！という意見がありましたら、ご教授ください。