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
これで一括保存とバリデーションを両立することができます.