QA@IT

has_oneの関係のテーブルを同時にupdateしようとした際、foreign_keyを更新しようとして失敗する

11568 PV

追記(3/3)

labochoさんの回答で期待通りの動作になりましたが、さらに調べてみた所 accepts_nested_attributes_forupdate_only オプションを true にすることで同じ動作にすることができることを確認しました。

http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html#method-i-accepts_nested_attributes_for

http://stackoverflow.com/questions/18984093/cant-update-my-nested-model-form-for-has-one-association

追記(2/28)

以下のリクエストで更新を行うことができました。
labochoさん、mori_devさん、ありがとうございました!

post :update, { :id => user.to_param, :user => { "email" => "new@example.com", :user_information_attributes => { "name" => "hoge" } }

しかし、下記のように子テーブルの更新できる要素が複数ある場合、小テーブルの片方の要素のみ更新することができませんでした。

  • user_informations
| column   |
|:--------:|
|id        |
|user_id   |
|first_name|
|last_name |
  • リクエスト
post :update, { :id => user.to_param, :user => { "email" => "new@example.com", :user_information_attributes => { "last_name" => "fuge" } }
  • rspecの結果
Failures:

  1) Api::UsersController POST /api/users/:id #update success behaves like http code returns http 200
     Failure/Error: Unable to find matching line from backtrace
     ActiveRecord::StatementInvalid:
       PG::NotNullViolation: ERROR:  null value in column "first_name" violates not-null constraint
       DETAIL:  Failing row contains (9906, 9925, null, fuga, 2014-02-28 15:45:30.630694, 2014-02-28 15:45:30.630694).
       : INSERT INTO "user_informations" ("created_at", "last_name", "user_id", "updated_at") VALUES ($1, $2, $3, $4) RETURNING "id"
     Shared Example Group: "http code" called from ./spec/controllers/api/users_controller_spec.rb:168
     # ./app/controllers/api/users_controller.rb:44:in `update'
     # ./spec/controllers/api/users_controller_spec.rb:154:in `block (5 levels) in <top (required)>'
     # -e:1:in `<main>'

子テーブルの更新を行う場合、必須の要素は全てパラメータに含める必要があるのでしょうか?
含める必要が無い書き方があればご教示いただきたいです。

また、updateなのにinsertを行ってる部分も気になります。


環境

  • osx 10.9.1(marvericks)
  • ruby 2.1.1
  • rails 4.0.2
  • postgresql 9.3.2
  • mysql 5.6.16

やりたいこと

@user.update(user_params)だけでuser_informationまで更新したいのですが、不可能でしょうか?

user_informationのインスタンスを作成してupdateするのも試したんですが、createの際は@user = User.new(user_params); @user.save(user_params)でうまくいったのでできるような気がするのですが…

ちなみにpostgresqlとmysqlの両方で試しましたが、同様の結果になりました。


現象

親テーブル: users
| column |
|:------:|
|id      |
|email   |
class User < ActiveRecord::Base
  has_one :user_information
  accepts_nested_attributes_for :user_information
end
子テーブル: user_informations
| column |
|:------:|
|id      |
|user_id |
|name    |
class UserInformation < ActiveRecord::Base
  belongs_to :user
end

userのcontrollerは下記のようになっています

module Api
  class UsersController < BaseController
    before_action :set_user, only: [:show, :edit, :update, :destroy]

    def update
      if @user.update(user_params)
        render json: @user
      else
        render json: @user, status: 404
      end
    end

    private

    def set_user
      @user = User.find(params[:id])
    end

    def user_params
      params.require(:user).permit(
        :email,
        :user_information_attributes => [
          :name
        ]
      )
    end
  end
end

上記の状態で下記リクエストを投げます

describe 'POST /api/users/:id', '#update' do
  let(:user) { Fabricate(:user) }

  context 'success' do
    before do
      post :update, { id: user.id, user: { email: "new@example.com", user_information_attributes: { name: "hoge" } }
    end

    it_behaves_like 'http code', 200
  end
end
Failures:

  1) Api::UserController POST /api/users/:id #update success behaves like http code returns http 200
     Failure/Error: Unable to find matching line from backtrace
     ActiveRecord::StatementInvalid:
       PG::NotNullViolation: ERROR:  null value in column "user_id" violates not-null constraint
       DETAIL:  Failing row contains (7702, null, Karen, 2014-02-27 18:47:57.95558, 2014-02-27 18:47:57.973278).
       : UPDATE "user_informations" SET "user_id" = $1, "updated_at" = $2 WHERE "user_informations"."id" = 7702     Shared Example Group: "http code" called from ./spec/controllers/api/users_controller_spec.rb:158
     # ./app/controllers/api/users_controller.rb:46:in `update'
     # ./spec/controllers/api/users_controller_spec.rb:155:in `block (4 levels) in <top (required)>'
     # -e:1:in `<main>'

すると、user_informationsuser_idnullで更新しようとする挙動が見られます。
また、そもそもnameを更新しようともしてないように見えます。

UPDATE "user_informations" SET "user_id" = $1, "updated_at" = $2 WHERE "user_informations"."id" = 7702

回答

UserInformation の id を渡していないのが原因かと思います。

下記の変更を加えたら、期待通り動作しました。

  def user_params
    params.require(:user).permit(
      :email,
      :user_information_attributes => [
+       :id,
        :name
      ]
    )
  end
  before do
-   post :update, { id: user.id, user: { email: "new@example.com", user_information_attributes: { name: "hoge" } }
+   post :update, { id: user.id, user: { email: "new@example.com", user_information_attributes: { id: user.user_information.id, name: "hoge" } }
  end

has_many だと、子レコードの id が無ければ作成、あれば更新とわかりやすいのですが、has_one で id 指定しない場合の挙動はちょっと私もよくわかりません。

編集 履歴 (0)
  • ありがとうございます、期待通りの動作になりました!

    また、この方法とは別に、`accepts_nested_attributes_for `の`update_only `オプションを`true`にすることでも同じ動作になることを確認しました。詳細は本文に追記します。
    -

まったく手元で検証していないのですが、
UsersController#user_params の :user_information_attributes で、has_one なのに配列を指定しているからではないでしょうか。たしか has_one のときは配列ではなかった気がします。違うかもしれません。。

編集 履歴 (0)
  • ありがとうございます、試してみます! -
ウォッチ

この質問への回答やコメントをメールでお知らせします。