Railsで関連モデルを同時に更新したい(has_oneで関連させていたモデルをbuild_associationで安直に生成しようとしたら痛い目をみた話)
Railsで関連モデルを同時に更新するようなコードを書こうとして、思いの外ハマって抜けられなくなったので備忘録。
関連モデルを同時に更新したい状況
例えばメールアドレスやパスワードのような認証情報と、生年月日や趣味のようなプロフィール情報を分けて管理したい場合など結構ありがちだと思います。 以下のような関連を持つモデルのようなイメージ。
とりあえず書いてみる
ひとまず自分の思うがままにコードを書いていく。
まずモデルから。
app/models/user.rb
class User < ActiveRecord::Base has_one :profile, dependent: :destroy accepts_nested_attributes_for :profile end
app/models/profile.rb
class Profile < ActiveRecord::Base belongs_to :user end
親モデルのUserにaccepts_nested_attributes_for
を設定することで親モデルから子モデルを作成したり、保存したりできるようにする。
次にコントローラ。
app/controllers/users_controller.rb
class UsersController < ApplicationController before_action :set_user, only: [:show, :edit, :update, :destroy] def index @users = User.all end def show end def new @user = User.new end def edit @user.build_profile end def create @user = User.new(user_params) @user.save redirect_to :users end def update @user.update(user_params) redirect_to :users end def destroy @user.destroy redirect_to :users end private def set_user @user = User.find(params[:id]) end def user_params params.require(:user).permit(:email, :password, profile_attributes: [:birthday, :hobby]) end end
viewでfields_for
を使うのでprofile_attributes
をストロングパラメータに記入しておく。
(fields_for
は[引数に与えた名前]_attributes
というname属性になるためらしい)
仕様としてプロフィール情報を入れるのはユーザ情報編集時のみとするため、登録しているUserモデルからProfileモデルを生成できるようにedit
に@user.build_profile
と何気なく記入。
親から子を生成するときはとりあえずbuildっしょとかいう素人考えでbuild_profileを記入したが 、後にどツボへの道へと誘うことになるとはこの段階では知る由もなかった…。
とりあえず次にビュー。
app/views/_form.html.erb
<%= form_for(@user) do |f| %> <% if @user.errors.any? %> <div id="error_explanation"> <h2><%= pluralize(@user.errors.count, "error") %> prohibited this user from being saved:</h2> <ul> <% @user.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <div class="field"> <%= f.label :email %><br> <%= f.text_field :email %> </div> <div class="field"> <%= f.label :password %><br> <%= f.password_field :password %> </div> <% unless current_page?(new_user_path) %> <%= f.fields_for :profile, @user.profile do |pf| %> <div class="field"> <%= pf.label :birthday %><br> <%= pf.date_field :birthday %> </div> <div class="field"> <%= pf.label :hobby %><br> <%= pf.text_field :hobby %> </div> <% end %> <% end %> <div class="actions"> <%= f.submit %> </div> <% end %>
プロフィール情報はnewのタイミングでは表示させないようにcurrent_page?
を設定。
fields_for
のタグを<%= %>
ではなく<% %>
で記入してプロフィール情報入力欄出ない問題にも軽くハマっているのは内緒の話。
これでとりあえず一通り完成!
てことで動作確認
ユーザを登録
次にプロフィール情報を入力するために編集
いい感じにプロフィール情報を入力できるようになっている。
とりあえずプロフィール情報を入力。
登録できたか確認
いい感じに登録できている。
しかし、問題が起きる…
登録までできて、「いやー、ひと仕事終わったよ」と清々しい気分でいましたが、再び編集画面を表示した際に違和感を覚える。
あれ…?プロフィール情報が表示されてなくね…?
いやいや、まさかそんなはずは…と思い、再びshowしてみるとProfileモデルなんて存在してねーけど…と怒られる。
DBも確認してみたが、profilesテーブルがまっさらになっている…
>> Profile.all Profile Load (0.5ms) SELECT "profiles".* FROM "profiles" => #<ActiveRecord::Relation []>
さっきまで存在していたプロフィール情報がどこか遠い国に旅立ってしまっている…
登録し直したり、デバッグしてみたりで原因を探ること小一時間。
問題点があらわになる。
@user.build_profile
を実行した後にレコードが消えている…!!!!!
サーバログを見てみると
Started GET "/users/8/edit" for ::1 at 2016-02-07 12:12:38 +0900 Processing by UsersController#edit as HTML Parameters: {"id"=>"8"} User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 8]] Profile Load (0.2ms) SELECT "profiles".* FROM "profiles" WHERE "profiles"."user_id" = $1 LIMIT 1 [["user_id", 8]] (0.1ms) BEGIN SQL (0.2ms) DELETE FROM "profiles" WHERE "profiles"."id" = $1 [["id", 7]] (2.0ms) COMMIT Rendered users/_form.html.erb (3.0ms) Rendered users/edit.html.erb within layouts/application (4.2ms) Completed 200 OK in 38ms (Views: 20.1ms | ActiveRecord: 2.7ms)
なんかDELETEが走っちゃってますけど!!!!
どうもbuild_profile
でProfileモデルを生成の際は一旦存在するレコードを消してから生成みたいなことが起きているっぽい。
ということでbuild_profile
を使わないように修正。
def edit @user.profile = Profile.new if @user.profile.blank? end
てな感じで無事プロフィール情報を登録してから編集画面にいっても情報が消えなくなりました。
とりあえず関連モデルを生成するならbuildじゃね?という安易な考えを悔い改めようと思いましたまる