複数モデルに同時にレコードを作成する(accepts_nested_attributes_forで1対1 ~has_one, belongs_to~ を扱う)
複数モデルに同時登録するときはaccepts_nested_attributes_forが便利。
ですが、ちょこちょこ実装するのにミスがあり、半日以上かかってしまった(m m)
エラーが起きた原因と解決策をざっくりとまとめました。
TD:TR;
- DB設計は大事
inverse_of
オプションはつけたほうが良い(と私は思う)update_only: true
オプションも使用した- ちなみにこんなgemもあるらしい - Rails フォーム(form_for,nested_form,fields_for) による複数同時投稿(親子、親子孫、他人) - Qiita
開発環境
ざっくり備忘録にする理由
- いろんな記事を参考にしてもそれとは違うエラー原因だった
- エラー解決まで非常に時間を要したため
ざっくりとした全体像
- 今回は2モデル~1対1~間でのお話(
has_one
,belongs_to
)- user, birthモデルとする
今回は下記がメイン
- new, create, edit, updateアクションの処理
- Activerecordの書き方
テスト(Rspec)は書かない
前準備
アプリ作成、DB作成
# gemのインストールする場所を指定 bundle config set path 'vendor/bundle'; # railsをインストール gem install -v 5.1.7 rails # railsアプリを作成 rails _5.1.7_ new tmp_app --database=mysql; # DB作成 rails db:create # userモデルの作成 rails g scaffold User name:string; # birthモデルの作成 rails g scaffold Birth area:string; # migrationの実行 rails db:migrate
先に完成後のコード一覧
- ざっくりと大事そうなとこだけ載せてる
- フォームはUserモデル側で作成してある
- 以下は省略
- viewファイル(index, create, show, update, destroy)
- BirthモデルにUserの外部キーを追加すること
app/models/user.rb
class User < ApplicationRecord has_one: birth, inverse_of: user accepts_nested_attributes_for :birth, update_only: true end
app/models/birth.rb
class Birth < ApplicationRecord beglongs_to: user, inverse_of: birth end
app/views/users/new.html.erb
<%= form_with(model: @user, url: user_path, local: true) do |form| %> <%= render 'user_form', {form: form , user: @user} %> <div> <%= form.submit %> </div> <% end %>
app/views/users/_user_form.html.erb
<div class="form-group"> <%= form.label :name %> <%= form.text_field :name, id: :user_name %> </div> <%= form.fields_for :birth do |d| %> <%# エリア %> <div class="form-group"> <%= d.label :area %> <%= d.text_field :area %> </div> <% end %>
app/controllers/users_controllers.rb
class UsersController < ApplicationController before_action :set_user, only: [:edit, :update] def index @users = User.all end def new @user = User.new @user.build_birth end def create @user = User.new(user_params) if @user.save redirect_to users_path, flash: { notice: "作成されました" } else render 'new' end end def edit end def update if @user.update(user_params) redirect_to users_path, flash: { notice: "変更されました" } else render 'edit' end end private def set_user @user = User.find(params[:id]) end def user_params params.require(:user).permit(:name, birth_attributes: [:area]) end end
エラーとその解決法
問題点1(エラー ActiveModel::UnknownAttributeError - unknown attribute 'xxx_id' for yyy.
)
- newアクションで
unknown attribute 'user_id' for Birth.
が発生 build_birth
メソッドでエラーが発生する- 下の記事はわかりやすかったです
build_yyy
とyyy_attributes=
について - ruby on rails - accepts_nested_attributes_forで生えたメソッドとcreateやupdateの関係性 - スタック・オーバーフロー
# app/controllers/users_controllers.rb : def new @user = User.new # ↓↓↓ ここでエラーが発生 @user.build_birth end :
問題点1の原因と解決法
- DB設計が原因(Birthに外部キーをつけ忘れる ~user_id~)
- Birthモデルに外部キー(user_id)を追加する(migrationファイルで編集・修正する)ことでエラーが解消される(newアクションが表示される)
作成時(bad)
user | Birth | |
---|---|---|
id | id | |
name | area |
修正後(good)
user | Birth | |
---|---|---|
id | id | |
name | area | |
user_id |
問題点2(エラー ActiveRecord::RecordNotSaved - Failed to remove the existing associated nutrient. The record failed to save after its foreign key was set to nil
)
- editアクションでエラー
- accept_nested_attributes_forでまとめてuserとbirthモデルのレコードを更新しようとしているために起きるエラーみたいです
- guguruとそのまま出てきました
- 私は
- 両方のモデルに
inverse_of
オプションを指定 - [rails] accepts_nested_attributes_for の場合は、inverse_of を付けておくと良い気がする - Qiita
- 両方のモデルに
update_only: true
を選択しました(この記事はおすすめです) - has_oneの関連づけをそのまま更新しようとして "The record failed to save after its foreign key was set to nil." と言われた - Qiita
問題点2の原因と解決法
- ただしエラーの原因は
edit
アクション- newアクションでは
build_yyy
メソッドは問題ないが、editアクションでは必要ない(-> 削除する)
- newアクションでは
# app/controllers/users_controllers.rb : def edit # ↓↓↓ ここでエラーが発生(必要なかった) @user.build_birth end :
- モデルで
inverse_of
を上手いこと定義すると、userインスタンスに関連するbirthレコードを取得してきてくれる - たぶんこれで大丈夫
- app/models/user.rb
class User < ApplicationRecord has_one: birth, inverse_of: user accepts_nested_attributes_for :birth, update_only: true end
- app/models/birth.rb
class Birth < ApplicationRecord beglongs_to: user, inverse_of: birth end <br> <br> 以上!!