【Rails6.1】AIチャットと一緒に在庫を減らす処理を考えた話

# はじめに
Railsの学習中に、予約時に在庫を減らす作業が分からなかったので、調べたりAIに聞いたりしながら完成させた記述について記録します。

# 結論
在庫を減らす処理には、「decrement!」というレコードの値を減らすことのできるメソッドを使用しました。
```ruby:
item = Item.find_by(params[:pre_order][:item_id])
@pre_order.amount = params[:pre_order][:amount] #予約数:amountをフォームから受け取る
item.decrement!(:stock, @pre_order.amount) #decrement!でitemの在庫:stockを減らす
```

# 記述修正の流れ
最初、自分で調べたり考えてみたりしてみたのですが、どうにも上手くいかなかったのでAIチャットに聞いてみることにしました。

## AIへの質問
「railsにおいて予約機能を作成中です。
@&#8203;pre_orderがsaveできたらitemレコードのstockカラムの数を1個減らす処理を行いたいです。どのように記述を修正すればいいでしょうか？」

というコメントと共に以下のコードをAIチャットへと送り出しました。
（ちなみに、今回はCopilotチャットを使用しています。）

```ruby:pre_orders_controller.rb
def create
    @pre_order = current_member.pre_orders.new(pre_order_params)
    item = Item.find_by(params[:pre_order][:item_id])
    @pre_order.item_id = item.id
    @pre_order.total_payment = item.price
    if @pre_order.save
      stock = item.stock
      stock.update!(stock - 1.to_i) #ここで引き算すれば良いんじゃないかという推測
      redirect_to thanks_path
    else
      render :new
    end
end
```
この記述の意図としては、stockの数を1個減らせば良いのでは…？という考えでした。
正直、計算式の書き方にも自信がないし、そもそもupdateで良いのだろうか…という疑問からAIに聞いてみることにしました。

## 返ってきた回答
以下がCopilotからの返答です。

>Railsで予約機能を実装する際、@pre_orderが保存された後にitemレコードのstockカラムを1減らす処理を行いたい場合、以下のように記述できます。
具体的には、after_createコールバックを使用して、@pre_orderが保存された後にitemのstockを減らす処理を実行します。

```diff_ruby:pre_orders_controller.rb
class PreOrdersController < ApplicationController
  def create
    @pre_order = current_member.pre_orders.new(pre_order_params)
    item = Item.find_by(params[:pre_order][:item_id])
    @pre_order.item_id = item.id
    @pre_order.total_payment = item.price
    if @pre_order.save
+     item.decrement!(:stock, 1)
      redirect_to thanks_path
    else
      render :new
    end
  end
end
```
Copilotからのメッセージにはちょっとズレた内容も混ざっていましたが、返ってきたコードを見ると「decrement!」という初めて見たメソッドが使われていました。
何者かがいまいち分からなかったので、軽く調べてみることにします。

## decrement!について調べる

>decrementとは特定のレコードをカウントダウンすることができるメソッド
：
Rails4以前のincrement!/decrement!は同時実行での処理が考慮されておらず、計算にズレが生じてしまうということがありました。

https://techtechmedia.com/increment-decrement-rails/

どうやら特定のレコードの値を減らすことのできるメソッドのようです。
知らないメソッドなのでもう少し調べてみると、

>以下のメソッドはバリデーションを行わずにスキップします。オブジェクトの保存は、有効無効にかかわらず行われます。これらのメソッドの利用には注意が必要です。

https://railsguides.jp/v6.1/active_record_validations.html#%E3%83%90%E3%83%AA%E3%83%87%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E3%82%B9%E3%82%AD%E3%83%83%E3%83%97

>バリデーション（検証）の場合と同様、以下のメソッドでもコールバックをスキップできます。

https://railsguides.jp/active_record_callbacks.html#%E3%82%B3%E3%83%BC%E3%83%AB%E3%83%90%E3%83%83%E3%82%AF%E3%82%92%E3%82%B9%E3%82%AD%E3%83%83%E3%83%97%E3%81%99%E3%82%8B


バリデーションとコールバックの内容に「decrement!」についての記述がありました。
また、バージョンによっても仕様が異なるようなので、使用する際に少し注意が必要かもしれません。

今回は数を減らしたいだけだったので、問題ないだろうと判断しこちらのメソッドを使ってみることにします。

## 記述の修正
実際にAIが生成したコードで記述してみると期待通りの動きになり、当初の問題は解決しました。
ですがここで、予約数を1個だけじゃなくて個数を選べた方が良いと気づき、選択された個数を在庫から減らす記述に変更することにしました。


## 予約時に選択された数を、在庫から減らす
AIに生成してもらった在庫を1個減らすこの部分の処理を、

```ruby:
item.decrement!(:stock, 1)
```

予約時に選択された個数（@pre_order.amount）を減らす処理に変更。

```ruby:
item.decrement!(:stock, @pre_order.amount)
```

実際に変更を施した記述がこちらです。

```diff_ruby:pre_orders_controller.rb
class PreOrdersController < ApplicationController
  def create
    @pre_order = current_member.pre_orders.new(pre_order_params)
    item = Item.find_by(params[:pre_order][:item_id])
    @pre_order.item_id = item.id
+   @pre_order.amount = params[:pre_order][:amount]
    @pre_order.total_payment = item.price

    if @pre_order.save
-     item.decrement!(:stock, 1)
+     item.decrement!(:stock, @pre_order.amount)
      redirect_to thanks_path
    else
      render :new
    end
  end
end
```

## 合計金額の処理も変更する
動作を確認して、きちんと予約数に合わせて在庫が減っていくことにひと安心したところ、pre_ordersコントローラ内の別のミスを発見しました。

もともと1個ずつしか予約できない設定だったので、
合計金額（total_payment）＝商品単価（item.price）になってしまっていました。

```ruby:変更前
@pre_order.total_payment = item.price
```
こちらも、合計金額（total_payment）＝商品単価×予約数の記述に変更。
```ruby:変更後
@pre_order.total_payment = item.price * @pre_order.amount
```
予約を受けたお店側が大損するところでした。危なかったです。

## 合わせてビューも変更する
ついでにビューでも、在庫が0の場合は個数選択が出来なくなるように設定することにします。
```html:show.html.erb
<tr>
  <th>在庫</th>
  <td><%= @stock %>個</td>
  <td>
    <% if @stock == 0 %>
      在庫切れのため購入できません
    <% else %>
      <%= f.select :amount, @stock_array, { prompt: "個数を選択" } %>
    <% end %>
  </td>
</tr>
```
これで無事に予約時の処理が完了です。

ちなみに、コントローラは最終的にこんな形になりました。
```ruby:
def create
    @pre_order = current_member.pre_orders.new(pre_order_params)
    item = Item.find(params[:pre_order][:item_id])
    @pre_order.item_id = item.id
    @pre_order.amount = params[:pre_order][:amount]
    @pre_order.total_payment = item.price * @pre_order.amount
    if @pre_order.save
      item.decrement!(:stock, @pre_order.amount)
      redirect_to thanks_path
    else
      render :new
    end
end
```
※実際には、記録として予約時の商品名なども保存する設定にしていたのですが、見づらくなってしまうと思いこちらでは省いています。

# おわりに
AIチャットも活用しながら学習を進めているのですが、基本的にはCopilotやChatGPTの無料版を使っています。
最近は、CopilotはどうやらGPT-4らしいと聞いたり、参照記事をピックアップしてくれることからもCopilotを使いがちです。
AIツールに関する知識は全然ないのですが、文章作成や学習時などでもありがたみを感じています。
もっと便利なツールや使い方があったら、ぜひ知りたいです。