【Rails 6】form_with用いて一括登録する

  • このエントリーをはてなブックマークに追加

レコードの一括登録をしたかったのですが、form_withにモデルを紐付けるので、複数のレコードを一気に保存する方法が難しく時間かかりました。

備忘録として記事にしてみます。

やりたいこと

# Memoテーブル
# id : integer
# title : string

class Memo < ApplicationRecord
  validates :title, presence: true
end

このようなMemoモデルがあって、それのnewアクションを作るとき、普通の実装では以下になると思います。

それにちょっと手を加え、一つのフォームで入力欄を複数設け一括で登録したいです。

方針

一括登録するようにモデルを作って、そのなかで独自saveメソッドを作成する。独自saveメソッド内で元のモデルのインスタンスを複数作成する

コントローラーの実装

コントローラーのnewアクションではmemoモデルではなく、それを管理するMemoCollectionモデルを生成します。

MemoCollectionモデルは後述しますが、models/form/memo_collection.rbに作成する予定です。

class MemosController < ApplicationController
  def index
    @memos = Memo.all
  end

  def new
    @form = Form::MemoCollection.new
  end

  def create
    @form = Form::MemoCollection.new(memo_collection_params)
    if @form.save
      redirect_to new_memo_path
    else
      render :new
    end
  end

  private
    def memo_collection_params
      params
        .require(:form_memo_collection)
        .permit(memos_attributes: :title)
    end
end

ストロングパラメータにはmemos_attributesを受け取るようにします。

これはViewでfields_forを使用していく予定のためです。

fields_forについてはこちら

ビューの実装

viewではfields_forを使いましょう。

= form_with( model: @form ,url: memos_path, method: :post) do |f|
  = f.fields_for :memos do |i|
    = i.text_field :title
    
  = f.submit "送信"

f.fields_for :memosとしているので、@formであるForm::MemoCollectionモデルには、memosという属性を定義している必要が出てきます。次のモデルのところで実装予定です。

モデルの実装

Memo.rbは普通の実装です。

class Memo < ApplicationRecord
  validates :title, presence: true
end

追加で、Form::Baseを継承したForm::MemoCollectionのようなモデルを作成していきましょう。

models/form/memo_collection.rbに作成するのが良いと思います。

MemoCollectionモデルは、attr_accessor :memosとして、Form::MemoCollection.memosが使えるようにしておきましょう。

Form::MemoCollection.new(…)とするために、initializeメソッドを、Form::MemoCollection.saveとするために、saveメソッドを作成しましょう。

自作saveメソッド内で、ループしMemoモデルのインスタンスを送られてきたかずループしてsaveします。

class Form::MemoCollection < Form::Base
  FORM_COUNT = 3
  attr_accessor :memos

  def initialize(attributes = {})
    super attributes
    self.memos = FORM_COUNT.times.map { Memo.new() } unless self.memos.present?
  end
  
  # 上でsuper attributesとしているので必要
  def memos_attributes=(attributes)
    self.memos = attributes.map { |_, v| Memo.new(v) }
  end

  def save
    # 実際にやりたいことはこれだけ
    # self.memos.map(&:save!)

    # 複数件全て保存できた場合のみ実行したいので、transactionを使用する
    Memo.transaction do
      self.memos.map(&:save!)
    end
      return true
    rescue => e
      return false
  end
end

Form::MemoCollectionの、initializeメソッドでsuperを使いたいので、class Form::MemoCollection < Form::Base のように継承する必要があります。

なので、models/form/base.rbを作成しましょう!

class Form::Base
  include ActiveModel::Model
  include ActiveModel::Callbacks
  include ActiveModel::Validations
  include ActiveModel::Validations::Callbacks
end

参考資料

https://qiita.com/Ryoga_aoym/items/91a3940cfa4de268fca4

https://rails.densan-labs.net/form/bulk_registration_form.html

こちらも合わせて読みたい

  • このエントリーをはてなブックマークに追加

SNSでもご購読できます。

スポンサードリンク

コメント

  1. sakai より:

    初学者ですが、コメント失礼します。

    models/form/memos_collection.rbでの下記のコードはどのような意味なのでしょうか。
    コントローラーのprivate以下のメソッドで呼び出していると思うのですが、何をしようとしているのか疑問に思いました。だいぶざっくりした質問で恐縮ですが、ご返信頂ければ幸いです。

    def memos_attributes=(attributes)
    self.memos = attributes.map { |_, v| Memo.new(v) }
    end

    また、attribute = {}としている意味など教えて頂ければ幸いです。

    1. Ryuji_tech より:

      コメントありがとうございます!
      最近はスパムコメントしかなかったので、めっちゃ嬉しいです。
      今回は、
      def memos_attributes=(attributes)
      self.memos = attributes.map { |_, v| Memo.new(v) }
      end
      の意味ということですね!

      結論からお伝えしますと、saveをする際にForm::Baseクラスのセッター(xxxx_attributes=)が呼ばれるのを上書きしている部分でして、viewでfields_forを使っているため必要になります。

      viewでfields_forを利用すると2つの条件が必要となります。
      1. fields_for の第一引数に渡した変数名の変数にアクセスできること
      2. 指定した変数が xxxx_attributes= (xxx は変数名)という形式で更新できること

      1.は今回であれば、:memosですね!(なので、models/form/memos_collection.rbでは、attr_accessorが必要になります。)

      2.はfields_forを使用するとparamsとして、xxxx_attributesというキーで送られます。
      Form::Baseクラスには、セッター(xxxx_attributes)があり、saveメソッドを使用するとセッターが呼ばれます。
      今回やりたいことは、fields_forで送られてきた複数のデータを
      self.memos = attributes.map { |_, v| Memo.new(v) }
      のようにしてMemo.new(v)することです。そのため、セッター(xxxx_attributes)を上書きしました。

      また、attribute = {}というのは、以下の部分ですかね?
      def initialize(attributes = {})

      initializeメソッドは、
      Form::MemoCollection.new(memo_collection_params)
      をした際に呼びだされますが、
      memo_collection_paramsがnilだった場合困るので、{}として初期化しています。
      memo_collection_paramsに値が入っていた場合は、 = {}は無視されます。

      回答になってますでしょうか?
      ややこしい実装になっていて申し訳ないです-_-
      不明点あれば追加で連絡いただければ大変ありがたいです!

Ryuji_tech へ返信する コメントをキャンセル

*