TIL: Rails has_one Nested Attributes Tweaking

In a project I'm working on right now I've been using a Rails nested form and a couple of things caught me off guard.

has_one Nested Form Sending id Attribute

In this case I have a nested form that is in a has_one relationship with the parent model. I think this is a common thing to do, especially if you want to offload less-frequently-accessed data to an auxiliary table in your database. For this example we have a User that can define some customizations in the product. A User has_one Customization as it were.

On a settings page I would like the User to be able to update some User things as well as some Customization things. Thus there is a nested set of form fields:

<%= form.fields_for :customization do |customization_form| %>
<%= customization_form.label :primary_color do %>
<%= customization_form.color_field :primary_color %>
Primary color
<% end %>
<%= customization_form.label :background_color do %>
<%= customization_form.color_field :background_color %>
Background color
<% end %>
<% end %>

This works well, save for one thing that bothered. In my Rails logs I saw (edited for readability):

Unpermitted parameter: :id. Context: { controller: UsersController, action: update, request: #<ActionDispatch::Request:0x000000010b4832b0>, 
params: {"_method"=>"patch", "authenticity_token"=>"[FILTERED]",
"user"=>{"name"=>"Barry Hess",
"customization_attributes"=>{
"primary_color"=>"#363b5c", "background_color"=>"#fdffb2", "id"=>"10"}},
"button"=>"", "controller"=>"users", "action"=>"update", "id"=>"3"} }

It's not entirely clear from that message, but the problem is the customization_attributes: "id" parameter. In this case I don't need an id to find my has_one Customization record. There us one and only one!

At the bottom of the Rails Guides documentation for nested forms I found this lovely sentence:

If the associated object is already saved, fields_for autogenerates a hidden input with the id of the saved record. You can disable this by passing include_id: false to fields_for.

And thus begat the new intro to the nested form:

<%= form.fields_for :customization, include_id: false do |customization_form| %>

Yay!

has_one Deleting Its Record and Recreating It Upon Save

After resolving that issue I was struck with another surprising set of log messages. Some smart past me must have figured out how to get colorization in my logs because in bright red I was seeing that this simple form update was deleting my Customization record:

Customization Destroy (0.4ms)  DELETE FROM "customizations" WHERE "customizations"."id" = $1  [["id", 10]]

This was followed by an assocationed INSERT INTO "customizations". I didn't really feel like this was representing what was happing in the application at all! So I googled "rails has_one delete and recreate table" and I found a handy Stack Overflow post linking to some documentation for accepts_nested_attributes_for:

:update_only
By default the :update_only option is false and the nested attributes are used to update the existing record only if they include the record's :id value. Otherwise a new record will be instantiated and used to replace the existing one.

On my User model I chose to go with:

accepts_nested_attributes_for :customization, update_only: true

There Is a Way with Less Code, but Is It Better?

Both of these items could have been solved if I simply allowed customizations#id as a permitted parameter at the controller level.

def user_params
params.require(:user).permit(
:name, :email,
customization_attributes: [
:id, :primary_color, :background_color
]
)
end

I just…don't feel great about that, though I'm not sure why. Certainly I would write some comments with params permissions including an id. Those comments would probably linking to this blog post.