Railsで一度のフォーム送信で複数レコードを新規作成・更新する方法

## はじめに

Railsアプリケーション開発において、一度のフォーム送信で複数のレコードを新規作成または更新するニーズは頻繁にあります。例えば、予約システムで一括でスケジュールを設定したり、注文フォームで複数の商品を同時に注文する場合などです。Railsの標準的なCRUD操作では、これらのシナリオを直接的に扱うことはできません。この記事では、独自のロジックを実装することで、このような複雑なフォーム処理をどのように実現できるかをステップバイステップで説明します。

## ユースケース

例として、予約システムにおいて、ユーザーが複数の予約を同時に編集・更新できる機能を実装することを考えます。各予約は、スタッフID、開始時間、終了時間などの情報を含みます。

## ステップバイステップの解説

### ステップ1: フォームの準備

```erb
<%= form_with url: time_table_path, method: :put, local: true, data: { controller: "staff-selector" } do |form| %>
  <% @reservation_services.each_with_index do |reservation_service, index| %>
    <%= reservation_service.service.name %>
    <%= format_duration(reservation_service.service.duration) %>
    <%= form.label "reservation_services_attributes[#{index}][staff_id]", 'スタッフ選択' %>
    <%= form.collection_select "reservation_services_attributes[#{index}][staff_id]", @working_staffs.select { |staff| staff.services.exists?(id: reservation_service.service_id) }, :id, :name, { include_blank: true }, { id: "staff_select_#{index}", data: { staff_selector_target: "staff", action: "change->staff-selector#updateSlots", slots_target_id: "time_slots_select_#{index}" } } %>
    <%= form.label :start_time, '利用可能な時間帯' %>
    <% if @working_staffs.any? { |staff| staff.services.include?(reservation_service.service) } %>
      <%= form.select "reservation_services_attributes[#{index}][start_time]", [], { include_blank: true }, { id: "time_slots_select_#{index}", data: { action: "change->staff-selector#updateEndTime", staff_selector_target: "slots", staff_selector_duration: reservation_service.service.duration } } %>
      <%= form.hidden_field "reservation_services_attributes[#{index}][schedule_id]", data: { staff_selector_target: "schedule_id" } %>
    <% else %>
      なし
    <% end %>
    <%= form.hidden_field "reservation_services_attributes[#{index}][duration]", value: reservation_service.service.duration %>
    <%= form.hidden_field "reservation_services_attributes[#{index}][reservation_service_id]", value: reservation_service.id %>
    <% if reservation_service.time_table.present? %>
      <%= form.hidden_field "reservation_services_attributes[#{index}][time_table_id]", value: reservation_service.time_table.id %>
    <% end %>
    <%= form.hidden_field :date, value: @reservation.date %>
    <%= form.hidden_field :reservation_id, value: @reservation.id %>
    <br>
  <% end %>
  <%= form.submit "予定を確認" %>
<% end %>
```

#### ステップ2: コントローラーのアクション定義

```ruby
  def update
    @reservation = Reservation.find(params[:reservation_id])
    params[:reservation_services_attributes].each do |_, reservation_service_attrs|
      # TimeTableを特定するためのIDを取得します。
      time_table_id = reservation_service_attrs[:time_table_id]
      @time_table = TimeTable.find(time_table_id)
      # スケジュールと時間の計算はcreateアクションと同様です。
      @schedule = Schedule.find_by(staff_id: reservation_service_attrs[:staff_id], date: @reservation.date)
      duration = reservation_service_attrs[:duration].to_i
      start_time_obj = Time.parse(reservation_service_attrs[:start_time])
      end_time_obj = start_time_obj + (duration * 15 * 60) # durationを分に変換してから秒単位に変更
      start_time = start_time_obj.strftime("%H:%M")
      end_time = end_time_obj.strftime("%H:%M")
      # TimeTableの属性を更新します。
      unless @time_table.update!(reservation_service_attrs
        .permit(:staff_id, :reservation_service_id)
          .merge(date: params[:date], schedule_id: @schedule.id, start_time: start_time, end_time: end_time, available: false))
        flash[:error] = "予約サービスの更新に失敗しました。"
        redirect_to reservation_path(@reservation) # 適切なパスにリダイレクト
        return
      end
    end
    flash[:success] = "予約サービスが正常に更新されました。"
    redirect_to reservation_path(@reservation) # 成功時の適切なパスにリダイレクト
  end
```

#### ステップ3: ルーティングの設定

```ruby
resources :time_tables
※後から気が付きましたが、アクションをtime_tables_conntrollerで定義すべきでした
```

### エラーハンドリングとセキュリティ

一括処理では、特にエラーハンドリングとセキュリティに注意が必要です。上記の例では、`update!`メソッドを使用しており、更新処理中に例外が発生した場合は即座に処理を中断し、エラーメッセージをユーザーに表示します。また、Strong Parametersを使用して、許可されたパラメータのみを受け取るようにすることで、不正なデータの更新を防ぎます。

### まとめ

一回のフォーム送信で複数レコードを新規作成または更新する機能は、複雑なWebアプリケーション開発において非常に便利です。この記事で紹介した手法を活用することで、Railsアプリケーションにおける一括処理の実装が容易になるはずです。