4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ネストされたフォームにより関連付けられたモデルにデータを同時保存する方法(Rails 6.0.0)

Last updated at Posted at 2020-05-24

##結論 親モデルにaccepts_nested_attributes_forを追加する
###実証環境
・Cloud9 Ubuntu Server
・Rails 6.0.0
・Ruby 2.6.3p62 (2019-04-16 revision 67580) [x86_64-linux]
###前提条件
次のような一対多のリレーションを持つParentモデルとKidモデルがあるとします。
※1:混乱を避けるため、「Parentって父親?母親?」とかは考えないものとします。
※2:モデル名に不規則変化の名詞を用いるとややこしいので、childではなくkidを用いています。
ペアレントモデルとキッドモデル

models/parent.rb
class Parent < ApplicationRecord
  has_many :kids, dependent: :destroy
end
models/kid.rb
class Kid < ApplicationRecord
  belongs_to :parent
end

###ネストしたフォームの実装
親モデルのデータ保存時に子モデルのデータも同時に保存するため、accepts_nested_attributes_for
をParentモデルに追加します。

models/parent.rb
class Parent < ApplicationRecord
  has_many :kids, dependent: :destroy
  accepts_nested_attributes_for :kids
end

これによって関連付けたデータを1つのフォームで登録できるネストしたフォームを使用できるようになります。

次に、親モデルのParentsコントローラーにて、フォームのページから送られてくるparamsを受け取る空のインスタンスを作ります。

その際、関連付けられた子モデルの空のインスタンスも作成します。
また、子モデルのpramsを受け取るために、ストロングパラメーターに
kids_attributes: [:name, :age, :toy]
を渡しています。

controllers/parents_controller.rb
class ParentsController < ApplicationController

#(中略)
  
  def new
    @parent = Parent.new
    @parent.kids.build #子モデルの空のインスタンスを作成
  end
  
  def create
    @parent = Parent.new(parent_params)
    if @parent.save
      redirect_to root_url
    else
      render :new
    end
  end

#(中略)
  
  private
  
    def parent_params
      #子モデルのパラメーターを受け取れるようにする
      params.require(:parent).permit(
        :name, :age, kids_attributes: [:name, :age, :toy] 
      )
    end

end
new.html.erb
<div class="container">
  <div class="col-sm-10 col-sm-offset-1">
    <h1 class="text-center">親登録</h1>
    <%= form_with(model: @parent, local: true) do |f| %>
      <div class="field form-group">
        <%= f.label :name %>
        <%= f.text_field :name, class: "form-control" %>
      </div>
      <div class="field form-group">
        <%= f.label :age %>
        <%= f.number_field :age, class: "form-control" %>
      </div>

      <!-- 子モデルのデータを受け取るためのネストされたフォーム -->

      <%= f.fields_for :kids do |kf| %>
      <h1 class="text-center">子登録</h1>
        <div class="field form-group">
          <%= kf.label :name %>
          <%= kf.text_field :name, class: "form-control" %>
        </div>
        <div class="field form-group">
          <%= kf.label :age %>
          <%= kf.number_field :age, class: "form-control" %>
        </div>
        <div class="field form-group">
          <%= kf.label :toy %>
          <%= kf.text_field :toy, class: "form-control" %>
        </div>
      <% end %>

      <div class="field form-group">
        <%= f.submit "上記内容で登録する", class: "btn btn-primary btn-lg btn-block" %>
      </div>
    <% end %>
  </div>
</div>

Bootstrapのクラスが入っており見にくいですが、上記のf.fields_forが子モデルのデータを受け取るフォームになります。
ネストされたフォーム
###データ更新(update)時に新たな関連付けられたデータを保存できるようにする
子モデル用の入力フォームですが、空のインスタンスを複数個作成することで、view自体のフォームは増やさずに入力欄を増やすことができます。
例えば、Parentsコントローラーを次のように変更することで、新規登録時には1組の子モデル入力フォーム、更新時には2組の子モデル入力フォームを表示させることができます。

controllers/parents_controller.rb
class ParentsController < ApplicationController

#(中略)

  def new
    @parent = Parent.new
    @parent.kids.build #子モデルの空のインスタンスを作成
  end
  
  def create
    @parent = Parent.new(parent_params)
    if @parent.save
      redirect_to root_url
    else
      render :new
    end
  end

#(中略)

  def edit
    @parent = Parent.find(params[:id])
    @parent.kids.build #子モデルの空のインスタンスを作成
  end
  
  def update
    @parent = Parent.find(params[:id])
    if @parent.update(parent_params)
      redirect_to root_url
    else
      render :edit
    end
  end
  
  def destroy
    @parent = Parent.find(params[:id])
    @parent.destroy
    redirect_to root_url
  end
  
  private
  
    def parent_params
      params.require(:parent).permit(
        :name, :age, kids_attributes: [:name, :age, :toy]
      )
    end
    
end

editアクションでも子モデルの空のインスタンスを作成することで、更新(update)時に「登録されている子モデルのデータ」+1個の入力フォームが作成されます。

これにより、JSなどで動的に追加するよりも簡便に入力フォームを増やすことができます。(個数が限定されてしまうのがネックですが...)

追加されるフォームをさらに増やしたい場合は、生成する空のインスタンスの数を
@parent.kids.build
から
n.times { @parent.kids.build }
とすることで、n個の入力フォームを生成できます。
###入力されなかった場合の空データを保存されないようにする
しかし、このままでは追加されたフォームに入力されなかった場合に空のデータが保存されてしまいます。
それを防ぐためにはParentモデルに追加したaccepts_nested_attributes_forの第2引数に次のようなProcを渡します。

models/parent.rb
class Parent < ApplicationRecord
  has_many :kids, dependent: :destroy
  accepts_nested_attributes_for :kids, reject_if: lambda {|attributes| attributes['name'].blank?}
end

上記の場合、子モデル入力フォームのnameが空の場合は他の属性(ここでは、age、toy)が入力されていても全て保存されません。

nameまたはageが入力されていない場合に登録させたくない場合は次のように変更します。

models/parent.rb
class Parent < ApplicationRecord
  has_many :kids, dependent: :destroy
  accepts_nested_attributes_for :kids, reject_if: lambda {|attributes| attributes['name'].blank? || attributes['age'].blank?}
end

上記に加え、子モデルへのバリデーションも変更すれば、保存される値を自由に制御できます。

この他、関連付けた子モデルのデータのみを削除したい場合などもあると思いますが、詳しくはRailsガイドの「Action View フォームヘルパー」がとても参考になります。
###あとがき
現在Ruby on Rails学習中の初学者・タツロンです。
自作アプリ作成中に数日間に渡って詰まった機能に関して投稿させていただきました。
間違っている点やアドバイス等ございましたらコメント頂けば幸いです。

4
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?