has_manyで関連づけられたモデルも一括保存する方法

Nested Attributesによって, has_manyで関連づけられたモデルのデータも丸ごと保存する方法を紹介します.

モデル例

本記事では例として以下のようなモデルを扱うことにします.

class User < ApplicationRecord
  has_many :posts
end

class Post < ApplicationRecord
  belongs_to :user
end

実現したいこと

やりたいことは, UserといくつかのPostをフォームから同時に登録することです. もう少しだけ正確に言うと, Userの登録時に, いくつかのPostもまとめて登録できるようにします. つまりメインのデータはUserということです.

では実装すべきコードを順に見ていきましょう.

モデルの実装コード

以下のようにUserモデルに1行追加します.

class User < ApplicationRecord
  has_many :posts
  accepts_nested_attributes_for :posts
end

これで, Postも一緒に保存するための受け入れ体制ができました.

またPostモデルからは, (もし書いていたら)以下のバリデーション を取り除きます.

class Post < ApplicationRecord
  belongs_to :user
  # validates :user_id, presence: true
end

当然湧いてくるであろう疑問

  • なぜこのバリデーションを除く必要があるのか
  • バリデーションはどのように定義すればいいのか

については最後に説明しますのでご心配なく.

コントローラの実装コード

次はコントローラです.

class UsersController < ApplicationController
  def new
    @uesr = User.new
    2.times { @user.posts.build }
  end 
  
  def create
    @user = User.new(user_params)
    if @user.save
      flash[:success] = 'User created!'
      redirect_to root_url
    else
      render 'new'
    end
  end
  
  private
    
    def user_params
      params.require(:user).permit(:name, :age, posts_attributes: [:title, :body])
    end
end

細かいところは仕様によって変わるでしょうが, 最低限上記のような実装は必要になると思います. 詳しく見ていきましょう.

newアクション

newアクションでのポイントは以下のコードですね.

2.times { @user.posts.build }

単にUser.newで終わらせるのではなく, 空のPostオブジェクトもbuildしておく必要があります. 今回は例としてPostを2つ作成しましたが, ここは仕様に左右されるところでしょう.

Strong Parameters

Postモデルも保存するわけですから, Strong Parametersにおいてこれを許可しておかなくてはなりません. 書き方は以下の通りです.

posts_attributes: [:title, :body]

attributesは複数形です. また今回はUser has many Postsという関係ですから, postsも複数形となります.

createアクション

ここまでできていれば, Postオブジェクト込みのUserオブジェクトを作成することは簡単です.

User.new(user_params)

Userオブジェクト単体で作成するときと同じですね.

コントローラはどんな形でパラメータを受け取るのか

モデルとコントローラが実装できれば, integration testを書くことができます. そのためには, コントローラがどんな形でパラメータを受け取るのかを知る必要がありますね. その答えは以下の通りです.

params: { user: {
  name: 'Ruby on Rails',
  age: 14,
  posts_attributes: [
    { title: 'hey', body: 'Lorem ipsum' },
    { title: 'hi', body: 'Thank you!'}
  ]
} }

ビューの実装コード

フォームのコード例です.

<%= form_for @user do |f| %>
  <%= f.label :name %>
  <%= f.text_field :name %>
  
  <%= f.label :age %>
  <%= f.number_field :age %>
  
  <%= f.fields_for :posts do |post_fields| %>
    <%= post_fields.label :title %>
    <%= post_fields.text_field :title %>
    
    <%= post_fields.label :body %>
    <%= post_fields.text_field :body %>
  <% end %>
  
  <%= f.submit "Submit" %>
<% end %>

簡単ですね! コントローラで2.timesとしているので, 自動的にPost用のフォームが2つ作られます.

Postモデルのバリデーション

はじめにPostモデルから以下のバリデーションを取り除きました.

validates :user_id, presence: true

これについて深掘りしていきます.

なぜこのバリデーションを取り除く必要があるのか

accepts_nested_attributes_forにより子モデルであるPostもまとめて保存できるようにした場合, 以下の流れで処理が進みます.

  • Userのバリデーション
  • Postのバリデーション
  • Userの保存処理
  • Postの保存処理

これは以下の記事を参考にしました. Rails5でnested attributesに詰まった話 - ウェブエンジニア珍道中

この記事にもあるように上のバリデーションを適用してしまうと,

Postのバリデーションの段階で, 未だ作られていないUserオブジェクトのidを要求してしまうため, 必ずバリデーション失敗する.

ということが起きてしまうわけです.

どのようにバリデーションを定義すべきか

失敗してしまうからといって, user_idの存在性を諦める必要はありません. ちゃんと別の方法が用意されています. 各モデルを以下のように編集しましょう.

class User < ApplicationRecord
  has_many :posts, inverse_of :user
  accepts_nested_attributes_for :posts
end

class Post < ApplicationRecord
  belongs_to :user, inverse_of :posts
  validates_presence_of :user
end

これで一括保存とバリデーションを両立することができます.