Why Rails params Seem Wrong

Rails handling of params seems inconsistent to me. Here's why:

Parameters: {
  "utf8"=>"✓", 
  "authenticity_token"=>"+qprtWg52V1tSRd0mizrbzc6p+jLog+BbpVmtPF7SKg=", 
  "parent"=>{"name"=>"Parent 1", 
    "children_attributes"=>{
      "0"=>{"child_name"=>"Child 1", 
        "grand_children_attributes"=>{
          "0"=>{"grand_child_name"=>"Grand Child 1a", "id"=>"1"}, 
          "1"=>{"grand_child_name"=>"Grand Child 1b", "id"=>"3"}}, 
      "id"=>"1"}, 
      "1"=>{"child_name"=>"Child 2 ", 
        "grand_children_attributes"=>{
          "0"=>{"grand_child_name"=>"Grand Child 2a", "id"=>"2"}, 
          "1"=>{"grand_child_name"=>"Grand Child 2b", "id"=>"4"}}, 
       "id"=>"2"}}}, 
    "commit"=>"Update Parent", "id"=>"1"}

These are the params for a set of nested models, "Parent", "Child", and "GrandChild". There is one parent, and multiple children & grand_children.

What seems odd to me is the fact that the top level parent class has their :id as a separate data structure from every other attribute. It's sitting there down at the end, next to "Update Parent", which is the submit button text. It's why update controllers look like this:

  # PUT /parents/1
  # PUT /parents/1.json
  def update
    @parent = Parent.find(params[:id])  #We're finding the Parent with this "disembodied" :id

    respond_to do |format|
      if @parent.update_attributes(params[:parent])  #  Then here, we are updating the data.
...

In this PUT update action, you can see we find the record we are looking for with an :id value that is not contained inside the "parent" params hash. It's a separate value, not contained within the params[:parent] hash.

What about the children_attributes, or the grand_children_attributes hashes? Do they also have some sort of disembodied :id? Nope. The :id for children_attributes and :grand_children_attributes is sitting right alongside the other data attributes for these models.

This means there is this odd disconnect when you are writing methods like "children_attributes=(attrs)". You find the :id right along with the other data. This feels like the Right Way.

So, instead of params & controller action that look like this for the parent:

"parent" => {"name"=>"Parent 1", "children_attributes"=>"...a lot here"}  #No :id here
And the controller action starts out like this:
@parent = Parent.find(params[:id])

They should look like this:

"parent" => {"name"=>"Parent 1", "children_attributes"=>"...a lot here", "id"=>"1"}
Now the controller action looks like:
@parent = Parent.find(params[:parent][:id])

Why does params[:parent][:id] NOT exist, but params[:parent][:children_attributes][:id] & params[:parent][:children_attributes][:grand_children_attributes][:id] do? Using the logic of the ultimate parent class, the :id for a child would be found in params[:parent][:id]. But, OH NOE! That doesn't work, because it would be ambiguous which child the :id belonged to!

That's why it doesn't seem consistent to me. The :id of ANY model should be contained inside the hash of attributes, not as an extraneous variable. The way we're doing it now seems a violation of how data should be structured. If you think of rails params as proper JSON, the current method of excluding the :id of the top-level parent just seems wrong. As it stands, the :id key-value pair in params is just as related to the Parent class as the :utf8, :authenticity_token, or :commit key-value pairs! The :id key is sort of an attribute of the params hash itself, not the parent class.

"But Wait! The code in the 'def update' would be slightly longer!! Oh, the horror!", you say. OK, but it's still weird. The way Rails handles child model params is totally different from the top-level parent. Why?

Just askin'.

Comments

The reason, of course, is because the root ID is coming in through the URL, like /posts/1, and not through the form data as every other piece of data in the example. I personally like that distinction. It makes it clear what's being used to fetch the root and what's part of the form data. But if you don't like that, you could just submit the ID through the form data as well, then you can get the parity you desire here.

I see your point.

But , to me, it feels like it is part of the form data, whether it is in the form action URL or not. Seems like in the course of determining the route (RESTful, of course), you are going to know the model, and that the :id should be part of the Model data, IMHO.

Wow... DHH at 3 minutes past midnight on Christmas! Ahhh... bleary eyed fathering at all hours... I remember it well.

For some reason the comments (2 spammers, and 1 DHH) are now gone. WTF. Anyway..

I guess I like the idea of params coming in the following form:

"parent"=>[{"name"=>"Name 1", "id"=>"1"}, {"name"=>"Name 2", "id"=>"2"}],
"child"=>[{"child_name"=>"Child Name 1", "id"=>"1", "parent_id"=>"1"},{"child_name"=>" Child Name 2", "id"=>"2", "parent_id"=>"1"}],
"grand_child"=>[{"grand_child_name"=>"Grand Child Name 1", "id"=>"1", "child_id"=>"1"},{"grand_child_name"=>"Grand Child Name 2", "id"=>"2", "child_id"=>"1"}],
etc...

Then controller actions can be reduced to (pseudo code):

def save_all_models
params.each do |k,v|
v.each do |instance|
instance.constantize.find(v[:id]).save
end
end
end

Seems to me that this way the data is more "consistent" with a full representation of the object data. There's no hideous nested N-deep params data, everything is just a key/val list of the models being updated (or created? or deleted??), handled with a really simple controller action, across just about any controller.