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 theid
of the saved record. You can disable this by passinginclude_id: false
tofields_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 isfalse
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.