概要
accepts_nested_attributes_for を使って、とあるレコードの編集時に、関連レコードの新規作成/更新 を行うようにしたつもりだったのですが、なぜか常に新規登録扱いになってしまい、SQL発行時に Duplicate entry でエラーになる、という事象がおこっていました。
原因は大したことなかったのですが、これにハマったのが2度目だったので自戒のため記事に残しておくことにした次第です。よろしければお付き合いください。
環境
- Ruby 2.5.1
- Rails 5.2.2.1
結論
先に結論を書いておきます。
id
を permit メソッド内で許可していなかったから。 です。
ハマるときに限って、意外と原因は大したことなかったりします。
舞台設定
順に説明していきます。まず今回の Model と Controller について記載します。
Model
Parent
が親で、Child
が関連モデルです。名前がいまいちですがご容赦ください
class Parent < ApplicationRecord
has_one :child
accepts_nested_attributes_for :child, allow_destroy: true
end
class Child < ApplicationRecord
belongs_to :parent
self.primary_key = :parent_id
end
Controller
**問題の原因はここにあります。**ここではあえて問題が再現する状態のままにしています。
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 に対応したビューの一部です。このようなフォームを作成しています。
<%= 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 になります。
<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 とかどうなんだろう、ということをふと考えました。