LoginSignup
16
10

More than 3 years have passed since last update.

nested attributes 使用時に毎回新規登録が行われてしまう

Posted at

概要

accepts_nested_attributes_for を使って、とあるレコードの編集時に、関連レコードの新規作成/更新 を行うようにしたつもりだったのですが、なぜか常に新規登録扱いになってしまい、SQL発行時に Duplicate entry でエラーになる、という事象がおこっていました。

原因は大したことなかったのですが、これにハマったのが2度目だったので自戒のため記事に残しておくことにした次第です。よろしければお付き合いください。

環境

  • Ruby 2.5.1
  • Rails 5.2.2.1

結論

先に結論を書いておきます。
id を permit メソッド内で許可していなかったから。 です。

ハマるときに限って、意外と原因は大したことなかったりします。

舞台設定

順に説明していきます。まず今回の Model と Controller について記載します。

Model

Parent が親で、Child が関連モデルです。名前がいまいちですがご容赦ください:sweat_smile:

parent.rb
class Parent < ApplicationRecord
  has_one :child
  accepts_nested_attributes_for :child, allow_destroy: true
end
child.rb
class Child < ApplicationRecord
  belongs_to :parent
  self.primary_key = :parent_id
end

Controller

問題の原因はここにあります。ここではあえて問題が再現する状態のままにしています。

child_controller.rb
class ChildController
  def update
    if @parent.update(child_params)
      # リダイレクト先は適当に書いたただの例なので気にしないでください
      redirect_to detail_page_path(@parent)
    else
      render :edit
    end
  end

  def child_params
    params.require(:parent).permit(
      # parent側もいろいろ指定がありますがここでは省略してます
      child_attributes: %i[title _destroy]
    )
  end
end

問題の解消方法

前述コントローラの child_params メソッド内を下記のように変更すると解消します。結論で書いたとおりですが、id を permit メソッド内で許可するキーに追加しています。

- child_attributes: %i[title _destroy]
+ child_attributes: %i[id title _destroy]

そもそもこの id って?

まずはこちらをご覧ください。これは child_controller に対応したビューの一部です。このようなフォームを作成しています。

child.html.erb
<%= form_with(model: @parent, local: true) do |form_parent| %>
  <!-- parent のフォームは省略 -->
  <%= form_parent.fields_for :child, @parent.child.build do |form_child|%>
    <%= form_child.text_field :title, placeholder: 'なんか入力してくれ' %>
  <%end%>
<%end%>

そして、実際に生成される child 分のフォームは以下のような html になります。

item.html
<input placeholder="なんか入力してくれ" type="text" value="" name="parent[child_attributes][title]" id="parent_child_attributes_title">
<input type="hidden" value="2" name="parent[child_attributes][id]" id="parent_child_attributes_id">

おや?
parent[child_attributes][id] という、自分では作成した覚えのない input hidden のフォームが生成されています。これが id パラメータの正体のようです。value 属性に入っているのは childs の主キーである parent_id の値でした。

どうやらこの値を見て、新規登録か、既存レコードの更新かを区別しているようです。新規登録の際は value に何も入っていません。そのため、コントローラ側の処理で id を permit メソッド内で許可しておかないと、常に新規登録として扱われてしまう、ということです。

ここで注意が必要なのが、主キーのカラム名は parent_id であるにも関わらず、input タグの name 属性には必ず id という名前が使用される という点です。DBのカラム名でそのまま考えると permit には parent_id を書いてしまいがちなんですが、主キーが送信されているパラメータの名前は常に id という名前であるため間違えないように気をつけましょう。でないと僕の二の舞になります。こんなミスする人は僕だけかもしれませんが

参考

余談

とはいえ、Rails はほんとにDB保存系の処理を書くのが楽ですね。そもそも最近の ORM ってみんなこんなかんじなんでしょうか?Laravel の ORM とかどうなんだろう、ということをふと考えました。

16
10
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10