views:

54

answers:

2

Given the following two models:

class Company < ActiveRecord::Base
  has_many :departments
  accepts_nested_attributes_for :departments
end

class Department < ActiveRecord::Base
  belongs_to :company
end

I can now create a company and its departments in one go:

@company = Company.create! params[:company]

In this example params[:company] is expected to look like this:

params[:company] = {
                     :name => 'Foo Inc',
                     :departments_attributes => {
                       1 => { :name => 'Management' },
                       2 => { :name => 'HR' }
                     }
                   }

Notice the :departments_attributes key!

But if I convert to XML using @company.to_xml, I get the following:

<company>
  <id type="integer">1</id>
  <name>Foo Inc</activity>
  <departments type="array">
    <department>
      <id type="integer">1</id>
      <company-id type="integer">1</company-id>
      <name>Management</name>
    </department>
    <department>
      <id type="integer">2</id>
      <company-id type="integer">1</company-id>
      <name>HR</name>
    </department>
  </departments>
</company>

Notice that I here get the nested resources in a container node called <departments> - not <departments_attributes>!

Why this inconsistency and is there a way to make Rails accept a POST request using departments instead of departments_attributes as the wrapper?

Am I the only one who thinks this is important when creating API's? It's weird for non-rails folks, that the output can't also be used as input.

How have you solved this - if at all?

A: 

You could always implement a departments_attributes method, something like:

def departments_attributes
  departments.map{ |d| d.attributes }
end

Or something like this...

jordinl
+3  A: 

There is a good reason for this. When you create an association like has_many :departments, rails creates several methods for you, including departments, departments=, and so on.

Now let's take the case of nested_attributes. I don't know if you were aware of this, but the parameters you pass into the params hash aren't limited to just attributes. They'll work for any method. Take this example:

class Company < ActiveRecord::Base
  def iliketurtles= attrs
    attrs.split(/\s+/).each{|attr| self.send "#{attr}=", 'turtles'}
  end

  def iliketurtles
    self.attributes.select{|attr, value| value == 'turtles'}.join(' ')
  end
end

In this example, any model attributes I pass to iliketurtles= (as a space-delimited string) will set those attributes' values to "turtles". And calling "Iliketurtles" will give you a space-delimited string of attributes whose value equals turtles. Here's the interesting part. I can now include iliketurtles in my forms:

<%= f.text_field :iliketurtles %>

or params, directly:

params[:company] = {
  :name => 'Foo Inc',
  :iliketurtles => 'description type address'
}

So what nested_attributes does is create two more methods, departments_attributes and departments_attributes=, "accessors" or "setters and getters", basically. So there are methods created to handle nested attributes. The problem is, you can't name those accessors departments and departments= because those method names are already taken by the association.

Is there a better way to do this? Maybe, but it would require a fundamental change to the way rails converts params into attributes, and might limit what developers can do. The ability to use custom accessors is much more powerful than my turtle example would lead you to believe :)

Jaime Bellmyer
Good explanation! :)
Thomas Watson
(damn pressing enter in a comment submits it) ... I would have added a link to a gist containing a code-proposal for converting the "incomming" params to the correct format: http://gist.github.com/653925 - Let me know what you think! :)
Thomas Watson
That looks like it would work, and maybe you should consider rolling into a rails gem :) One thing I did find, when you're deriving the model you need to add ".constantize" after ".singularize". Otherwise, it's still just a string.
Jaime Bellmyer
Thanks - I might do that :)
Thomas Watson
I use eval(...) around the string instead of .constantize, which should also do the trick - Didn't think of that. But your way seems nicer - will modify
Thomas Watson