ひよっこエンジニアの雑多な日記

なんとかWeb系のエンジニアをやっています。

Railsで関連モデルを同時に更新したい(has_oneで関連させていたモデルをbuild_associationで安直に生成しようとしたら痛い目をみた話)

Railsで関連モデルを同時に更新するようなコードを書こうとして、思いの外ハマって抜けられなくなったので備忘録。

関連モデルを同時に更新したい状況

例えばメールアドレスやパスワードのような認証情報と、生年月日や趣味のようなプロフィール情報を分けて管理したい場合など結構ありがちだと思います。 以下のような関連を持つモデルのようなイメージ。

f:id:kimuraysp:20160207000912p:plain

とりあえず書いてみる

ひとまず自分の思うがままにコードを書いていく。

まずモデルから。

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のタグを<%= %>ではなく<% %>で記入してプロフィール情報入力欄出ない問題にも軽くハマっているのは内緒の話。
これでとりあえず一通り完成!

てことで動作確認

ユーザを登録

f:id:kimuraysp:20160207111430p:plain

次にプロフィール情報を入力するために編集

f:id:kimuraysp:20160207111542p:plain f:id:kimuraysp:20160207111643p:plain
いい感じにプロフィール情報を入力できるようになっている。
とりあえずプロフィール情報を入力。

登録できたか確認

f:id:kimuraysp:20160207111651p:plain
いい感じに登録できている。

しかし、問題が起きる…

登録までできて、「いやー、ひと仕事終わったよ」と清々しい気分でいましたが、再び編集画面を表示した際に違和感を覚える。
f:id:kimuraysp:20160207111542p:plain
あれ…?プロフィール情報が表示されてなくね…?

いやいや、まさかそんなはずは…と思い、再びshowしてみるとProfileモデルなんて存在してねーけど…と怒られる。
f:id:kimuraysp:20160207113010p:plain

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じゃね?という安易な考えを悔い改めようと思いましたまる