【Rails 5】モデルを一括登録する手順

##はじめに
一画面で一つのモデルを一括登録するのにかなり時間がかかったので備忘録として残します。
本記事の一番下に完成形のコードがありますので、コードだけ見たい方はそちらをご覧ください。



##作るもの
Nameが空白だったらエラー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375365/10280ffc-5b86-ff01-0c20-ccdce7c4e184.png)


Nameが全て入力されていれば一括登録ができる
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375365/6f317a34-f638-1802-e084-89544cfd088b.png)
↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/375365/e4d3fa4c-bfb1-4e35-14df-93070aeba8b1.png)


##前提条件
ユーザーモデル

|ユーザーモデル       |
|:----------------------:|
| name| 
| address| 
| age | 
```ruby:user.rb
class User < ApplicationRecord
  # 入力必須
  validates :name, presence: true
end
```

##一括登録する手順
1. 複数のユーザーを格納できるコレクションモデルを作成
2. 一画面で複数のデータを登録できるフォームを作成
3. コントローラーでフォームのデータを取得
4. 複数登録用ののsaveメソッドを作成

###1.複数のユーザーを格納できるコレクションモデルを作成

```ruby:qiita.rb
class UserCollection
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  extend ActiveModel::Translation
  include ActiveModel::AttributeMethods
  include ActiveModel::Validations
  USER_NUM = 5  # 同時にユーザーを作成する数
  attr_accessor :collection  # ここに作成したユーザーモデルが格納される

  # 初期化メソッド
  def initialize(attributes = [])
    if attributes.present?
      self.collection = attributes.map do |value|
        User.new(
          name: value['name'],
          address: value['address'],
          age: value['age']
        )
      end
    else
      self.collection = USER_NUM.times.map{ User.new }
    end
  end

  # レコードが存在するか確認するメソッド
  def persisted?
    false
  end
end
```

最初にユーザーコレクションクラスをモデル化します。
下記の記事が分かりやすかったのでこちらをご覧ください。

[普通のクラスをRailsのモデル化する](https://www.bokukoko.info/entry/2015/12/20/Rails_%E3%81%AE%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E9%96%A2%E3%81%97%E3%81%A6)

次に初期化時の処理です。
initializeメソッドでは引数があった場合、その引数を使って複数のユーザーモデルを作成します。
引数がなかった場合はUSER_NUMの個数分ユーザーモデルを作成します。
作成したユーザーモデルはユーザーコレクションのcollectionに配列として格納されます。
これで以下のプログラムを記述するとユーザーモデルが5つ生成されます。

```ruby:sample.rb
    @users = UserCollection.new
```
###2. 一画面で複数のデータを登録できるフォームを作成

```ruby:users_controller.rb
  def new
    @users = UserCollection.new
  end
```

```ruby:new.html.erb
<h1>New User</h1>

<%= form_with model: @users, url: users_path, local: true do |form| %>
  <% @users.collection.each do |user|%>
    <%= fields_for 'users[]', user do |field| %>
      <div class="field">
        <%= field.label :name %>
        <%= field.text_field :name %>
      </div>

      <div class="field">
        <%= field.label :address %>
        <%= field.text_field :address %>
      </div>

      <div class="field">
        <%= field.label :age %>
        <%= field.number_field :age %>
      </div>
      </br>
    <% end %>
  <% end %>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>


<%= link_to 'Back', users_path %>
```
form_withとfields_forを使って複数登録できるフォームを作成します。
ここではfields_forが複数のデータを登録する大きなポイントとなってきます。

form_withは１つのデータしか登録できず、複数のデータを一気に登録することができません。
その問題点を解消する為に使うのがfields_forです。

fields_forは複数のモデルを登録することができます。
今回のパターンではユーザーコレクションの配列をループして、配列の要素数分のfields_forを作成しています。

次にfields_forの引数についての解説です。
以下のようにすることで配列形式でユーザーモデルのデータを送信することができます。

```ruby:new.html.erb
# 抜粋
<%= fields_for 'users[]', user do |field| %>
```


```ruby:sample
# 実際に送信したパラメーター
"users"=>[
  {"name"=>"w", "address"=>"w", "age"=>"1"},
  {"name"=>"q", "address"=>"d", "age"=>"1"}, 
  {"name"=>"e", "address"=>"f", "age"=>"1"}, 
  {"name"=>"r", "address"=>"g", "age"=>"1"}, 
  {"name"=>"f", "address"=>"h", "age"=>"1"}
]
```
###3. コントローラーでフォームのデータを取得

「2.一画面で複数のデータを登録できるフォームを作成」で送信したユーザーモデルを取得します

```ruby:users_controller.rb
...
  def create
    @users = UserCollection.new(users_params)
    if @users.save
      redirect_to users_url
    else
      render :new
    end
  end

  private

    def users_params
      params.require(:users)
    end
```

```ruby:sample
# 実際に取得できるパラメーター
[
  {"name"=>"w", "address"=>"w", "age"=>"1"},
  {"name"=>"q", "address"=>"d", "age"=>"1"}, 
  {"name"=>"e", "address"=>"f", "age"=>"1"}, 
  {"name"=>"r", "address"=>"g", "age"=>"1"}, 
  {"name"=>"f", "address"=>"h", "age"=>"1"}
]
```
UserCollection.new(users_params)を実行することでUserCollectionにフォームで入力した値が格納されます。

###4. 複数登録用ののsaveメソッドを作成

```ruby:users_collection.rb
  attr_accessor :collection

  # コレクションをDBに保存するメソッド
  def save
    is_success = true
    ActiveRecord::Base.transaction do
      collection.each do |result|
        # バリデーションを全てかけたいからsave!ではなくsaveを使用
        is_success = false unless result.save
      end
      # バリデーションエラーがあった時は例外を発生させてロールバックさせる
      raise ActiveRecord::RecordInvalid unless is_success
    end
    rescue
      p 'エラー'
    ensure
      return is_success
  end
```
最後にsaveメソッドを作成します。
処理の流れですが、collectionにはフォームで入力した複数のユーザーモデルが配列で格納されています。
その配列をループで回し、ひとつずつ保存していきます。
全てのユーザーモデルをバリデーションチェックしたい為、saveメソッドを使用しています。
もし1つでもバリデーションエラーが発生した場合は、例外を発生させて保存したユーザーモデルをロールバックで戻します。
※transactionは例外しかキャッチしてくれません！

is_successは保存が成功したかを表しています。
最後の処理でis_successを返却し、下記のような処理の分岐に使用しています。

```ruby:users_controller.rb
...
  def create
    @users = UserCollection.new(users_params)
    if @users.save
      redirect_to users_url
    else
      render :new
    end
  end
```

これでsaveが成功していれば一括登録の完了です！

##まとめ
ここまでご覧いただきありがとうございました。
間違い等あれば指摘いただけると大大大感謝です。

##ソースまとめ
####モデル
ユーザーコレクションモデル

```ruby:users_collection.rb
# ユーザーのコレクションモデル
class UserCollection
  include ActiveModel::Conversion
  extend ActiveModel::Naming
  extend ActiveModel::Translation
  include ActiveModel::AttributeMethods
  include ActiveModel::Validations
  USER_NUM = 5  # 同時にユーザーを作成する数
  attr_accessor :collection

  # 初期化メソッド
  def initialize(attributes = [])
    if attributes.present?
      self.collection = attributes.map do |value|
        User.new(
          name: value['name'],
          address: value['address'],
          age: value['age']
        )
      end
    else
      self.collection = USER_NUM.times.map{ User.new }
    end
  end

  # レコードが存在するか確認するメソッド
  def persisted?
    false
  end

  # コレクションをDBに保存するメソッド
  def save
    is_success = true
    ActiveRecord::Base.transaction do
      collection.each do |result|
        # バリデーションを全てかけたいからsave!ではなくsaveを使用
        is_success = false unless result.save
      end
      # バリデーションエラーがあった時は例外を発生させてロールバックさせる
      raise ActiveRecord::RecordInvalid unless is_success
    end
    rescue
      p 'エラー'
    ensure
      return is_success
  end
end
```
####コントローラー
```ruby:users_controller.rb
  def new
    @users = UserCollection.new
  end

  def create
    @users = UserCollection.new(users_params)
    if @users.save
      redirect_to users_url
    else
      render :new
    end
  end

  private

    def users_params
      params.require(:users)
    end
```
####ビュー
```ruby:new.html.erb
<h1>New User</h1>

<%= form_with model: @users, url: users_path, local: true do |form| %>
  <% @users.collection.each do |user|%>
    <%= fields_for 'users[]', user do |field| %>
      <div class="field">
        <%= field.label :name %>
        <%= field.text_field :name %>
      </div>

      <div class="field">
        <%= field.label :address %>
        <%= field.text_field :address %>
      </div>

      <div class="field">
        <%= field.label :age %>
        <%= field.number_field :age %>
      </div>
      </br>
    <% end %>
  <% end %>

  <div class="actions">
    <%= form.submit %>
  </div>
<% end %>


<%= link_to 'Back', users_path %>

```