【Rails】実用的！予約数の上限を設定し数字で予約を管理するロジック

初めまして！！
プログラミングを学習を開始してから初めてのPFとして実際の飲食店で使用する予約システムを作成しました！

予約システムに関してはネット上に実際に記事があり、簡単にできそう！！と思っていたのですが実際に運用するにあたり考慮する点が多々あり設計の段階から苦労しました。

今回は特に苦労した設計、ロジックについて記述していきます！！

※この技術はプログラミング初学者が独自に考えた解決策の一例です。不都合な点やより良い方法があるかもしれません。あくまで参考程度にして頂ければと思います。


# この記事で伝えたいこと
実際に使える、予約システムのロジック

## 解決したい課題
実際に使う予約システムを作成するにあたり、予約数の上限を考慮する必要がある。

## 理由

実際に運用したところ、突然、予約が殺到し一気に日に100件、1,000件、10,000件など来てしまったら？

可能性は極めて低いが起こりうる事象です。

ECサイトでも同様です。
在庫以上の注文があり決済も済んでしまっているが商品がない…など起こりうる事象です。

実際に運用するにあたり上限数の考慮は必要だと思いました

上限を設定せずとも管理者が確認し承認するシステムにすれば成り立ちますが、それならばGoogleフォームやメール受付にする方法と変わらないかなと思いました。


## 解決方法

今回の結論としては
**予約情報を管理するテーブル**に加え、**予約人数の上限を管理するテーブル**を作成し**多:１のアソシエーション**を設定。
予約が送信される度に、設定した**予約人数の上限から予約人数をマイナスするロジック**を作成し、**予約人数の上限が０を下回る際には予約を正しく送信できなくする**という方法でこの問題を解決しました。

# 実装方法

以下、実装方法になります。

今回の実装にあたり、こちらの記事を参考にさせて頂きました！
[[Ruby on Rails] 他のテーブルを参照してidを取得し、外部キーとしてデータを挿入する](https://qiita.com/clora_ornitier/items/a6309e6089099e1763f0)

## ER図

![予約システムER図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1870530/11acfabe-14ce-924d-501c-d74bc7853ca4.png)

日付（`start_time`）毎に予約人数の上限(`remaining_seat`)を設定し、その日の予約人数の上限を０より小さくならないよう予約人数(`people`)を引いていく。

- 予約システムに応じ月毎や時間帯毎など予合わせ上限を設ける。
- 名前以外に予約の際に欲しい情報(メールアドレスや電話番号など)があればカラムを追加。

## ビュー

```ruby:new.html.erb
<%= form_with model: @reservation, url: reservations_path, local: true do |f| %>
    <%= f.label :name %>
    <%= f.text_field :name %>

    <%= f.label :start_time %>
    <%= f.date_field :capacity_id %>
    # フロントでは日付を選んでいるが送信する情報は日付のID

    <%= f.label :people %>
    <%= f.select :people,[["1名", 1],["2名", 2],["3名", 3],["4名", 4],["5名", 5]], {include_blank: "人数を選択して下さい"} %>

    <%= f.submit "予約する" %>
<% end %>
```

フロントのフォームでは日付(`start_time`)を選んでいますが、実際に送信されるデータはアソシエーションが結んであるcapacityテーブルの日付のID(`capacity_id`)です。（コントローラーにロジックを記載）

*日付のカラム名が`start_time`なのは`gem simple_calendar`を使用しているためです。

## モデル

```ruby:reservation.rb
class Reservation < ApplicationRecord
  belongs_to :capacity

  # 予約上限から予約人数をマイナスする計算式
  def decreased_capacity
    capacity.remaining_seat - number_of_people
  end
end
```

```ruby:capacity.rb
class Capacity < ApplicationRecord
  has_many :reservations

　# 予約上限がマイナスにならないようにバリデーションを設定
  validates :remaining_seat, numericality: { greater_than_or_equal_to: 0 }
end

```
※必須なバリデーションを除き、バリデーションは必要に応じて変わるため本記事では割愛し、アソシエーションとコントローラーではなくモデルに記述することが推奨されている計算式のみ記述。

## コントローラー

```ruby:reservations_controller.rb
class ReservationsController < ApplicationController
  def new
    @reservation = Reservation.new
  end

  def create
    capacity_id = Capacity.find_by(start_time: params[:reservation][:capacity_id]).id
    # 入力された値(日付)からCapacityテーブルでデータを探し、capacity_idに代入
    Reservation.transaction do
      @reservation = Reservation.create!(reservation_params.merge(capacity_id: capacity_id))
      # 代入したcapacity_idをmerge、外部キーとしてidの値を挿入し予約情報のデータと共に作成、保存
      @reservation.capacity.update!(remaining_seat: @reservation.decreased_capacity)
      # 作成保存した予約情報に紐づく日付の予約上限人数の値を更新する処理
      # 予約人数上限 - 予約人数　を計算し、計算結果の予約上限人数に更新
    end
    redirect_to reservations_path, success: 'ご予約が完了しました。'
  rescue StandardError
    redirect_to new_reservation_path, danger: 'ご予約ができませんでした。'
  end

  private

  def reservation_params
    params.require(:reservation).permit(:name, :people)
  end
end
```

予約送信のアクション時に**予約情報の作成、保存**と**予約上限人数の計算、値の更新**はどちらも成功しなければ成り立たないので**トランザクション**を使用します。

トランザクションについてはこちらの記事で詳しく記載されております。

[RailsのTransactionの使い方](https://qiita.com/huydx/items/d946970d130b7dabe7ec)

簡単にいうと複数のデータを同時に保存や更新を行う場合、どちらかが失敗した際にはどちらも失敗してくれます。

予約の情報は保存できたけど、予約上限人数がマイナスになってるから上限数は更新できない！
というエラーを防いでくれます！


# 留意点、デメリット

実際に運用するには

- 日程変更するには？
- 人数変更するには？
- キャンセル処理はただの削除でいいの？
- 臨時休業の際にも予約できちゃうのでは？

などなど多くの問題がありますが今回は予約数の上限を設定し数字で予約を管理するロジックの部分のみ執筆させていただきました！
機会があれば続きの執筆もしたいと考えております。

# 最後に

最後までお読みいただき誠にありがとうございました。

初めて執筆でしたので読みづらかったと思いますが改善していきたいと考えております。

PFを初めて作成するにあたり、多くの時間悩みましたので同じような悩みを持っている方やこの記事の考え方や解決方法が何かの参考になれば幸いです！