ActiveRecord#attributes= with nested records

Given an ActiveRecord A, that belongs_to a B, which in turn belongs_to a C, I can create a single form to edit all the attributes of these nested records in one go, leading to a params hash with structure like:

{"a" => { "b" => { "c_attr1" => "x", c_attr2" => "x" }, "b_attr" => "x" }, "a_attr" => "x" }

I thought code like this would work:

a = A.new a.b = B.new a.b.c = C.new

a.attributes = params[:a]

It doesn't: the attributes= tries to use normal assignment for all elements of params, leading to an AssociationTypeMismatch (i.e. B expected, got HashWithIndifferentAccess) when it tries to assign a nested Hash to a nested AR. I thought it would detect a nested AR and call its attributes= rather than plain =?

What is the best way to solve this? Is there a reason why adding a little bit more intelligence to attributes= wouldn't work?

Thanks in advance, Chris.

How do you think this would work inside AR? It's kind of not the way I understand association proxies to work.

This info from the rdoc may be of use (note the part about attr_protected), still I don't know whether you will get the behavior you want by assigning a hash:

Allows you to set all the attributes at once by passing in a hash with keys matching the attribute names (which again matches the column names). Sensitive attributes can be protected from this form of mass-assignment by using the attr_protected macro. Or you can alternatively specify which attributes can be accessed with the attr_accessiblemacro. Then all the attributes not included in that won’t be allowed to be mass-assigned.

I hadn't considered attr_protected, or what would happen to has_many collections. Should it be possible to assign to those via an attributes hash? Probably not.

I suppose I expected it to work for my example because AR does a reasonable job of providing a hash-like interface for accessing/ setting individual attriubutes, e.g.

a[:b][:b_attr] = "x"

and if a[:b] was really a hash, you would be able to assign it in one go, or assign a nested hash to a in one go...

In essence, it's easy to get a form helper to use a hash structure to get/set values of nested AR attributes. It doesn't seem to be so easy to get those values back into the AR structure.

So what is the preferred way?

If I don't try to mirror the structure, I can create variables to point to each layer and use those in the form, e.g.

# Controller @a = a @b = a.b @c = a.b.c

# View <% form_for @a ....   <% fields_for @b...   <% fields_for @c...

# Controller after post a = A.new(params[:a]) a.b = B.new(params[:b]) a.b.c = C.new(params[:c])

This seems a bit long-winded, although it does hide the structure from the View...?

So what is the preferred way?

If I don't try to mirror the structure, I can create variables to point to each layer and use those in the form, e.g.

# Controller @a = a @b = a.b @c = a.b.c

# View <% form_for @a .... <% fields_for @b... <% fields_for @c...

# Controller after post a = A.new(params[:a]) a.b = B.new(params[:b]) a.b.c = C.new(params[:c])

This seems a bit long-winded, although it does hide the structure from the View...?

I've never solved this particular problem but 37s did, and that's -- as I understand it -- what form_for was created to do.

Then in the controller, you could:

a = A.create(params[:a]) b = B.create(params[:b])

a << b b << C.create(params[:c])

b.save

Again, I'm not writing real code here, so the b.save may be redundant and this doesn't take failed validations or anything into account. You might also consider delegating this work to the "master" model where it might make more sense.

Checkout the complex forms series on http://railscasts.com/. It uses this concept for setting one-to-many values.