On 21/09/12 12:25 PM, Tomas Sedovic
wrote:
On
09/20/2012 02:20 PM, Martyn Taylor wrote:
Gents.
I'm having some trouble with getting nested resources working
properly
from bespoke XML/JSON in RESTful API. There seems to be some
fundamental problems with the way rails works, which means we
have to
write a lot of bespoke code to get it working, possibly even
having to
monkey patch Rails itself.
I've written an example rails app that highlights the problem
and put a
detailed description in the README you can find it here:
https://github.com/mtaylor/Rails-Nested-Resource-Issues
I've ran into this issue on IME. But it's going to affect us
across all
rails projects with REST APIs, so we could do with discussing
this issue
and coming up with the best possible solution that results in as
little
replication as possible.
Please read through the problem and let me know if you have any
better
ideas that the solutions I have proposed.
Regards
Martyn
I'm beginning to lean towards the custom serialization and
deserialization. We'll have explicit control over the input/output
formats which is a good thing for API stability.
However, there may be another issue that's reasonably close to
what we expected the default behaviour would be.
If I understand this correctly, the issue isn't really behaviour
of the `attribute=` setters but of `Model.new`[1] and
`@model.update_attributes`[2] methods.
We could insert a base class into the model inheritance chain and
either "fix" the methods there or add new ones that do bulk
attribute operations correctly.
Pseudocode:
class NestedModels < ActiveRecord::Base
def initialize(attributes = nil, options = {})
if attributes
attributes = attributes.clone
get_nested_attribute_names.each do |name|
attributes[name + '_attributes'] = attributes[name]
attributes.delete name
end
end
super.initialize(attributes, options)
end
def update_attributes(attributes, options = {})
# TODO, similar to initialize
end
def update_attributes!(attributes, options = {})
# TODO, similar to update_attributes
end
def get_nested_attributes
# TODO: use introspection to figure out attribute names passed
to accepts_nested_attributes_for in the model declaration
end
end
class User < NestedModels
has_many :addresses
accepts_nested_attributes_for :addresses
attr_accessible :name, :addresses, :addresses_attributes
end
now calling:
User.new({"name"=>"Joe Bloggs",
"addresses"=>[{"street"=>"Church Street"}]})
would work because ActiveRecord::Base would receive:
{"name"=>"Joe Bloggs",
"addresses_attributes"=>[{"street"=>"Church Street"}]}
This is similar to the monkeypatching proposal but it's safer. We
would not be modifying any of the Rails' internals. Rather, we'd
explicitly specify which models should have this modified
behaviour.
We could probably also do this by including a module with the
overrides instead of using inheritance.
Keep in mind that ActiveRecord provides builder methods for
associations [3]. In the example you used:
User.address.build(:street => "Church street")
or if you already have an instance of User:
@user.address.build(:street => "Church street")
There are similar methods for one-to-many associations, which have
the benefit of elements of collection not being persisted
immediately (as opposed to adding an element to a collection
directly).
If you are inclined to solve the problem on the model level, you
could add a factory method that accepts a hash, and returns a
fully-assembled instance, nested resources and all, with no monkey
patching involved.
-d
[1]:
https://github.com/rails/rails/blob/9b5309fb6819d8f2a1b31e44ba61e682272c7aa3/activerecord/lib/active_record/base.rb#L481
[2]:
https://github.com/rails/rails/blob/81542f95d25825a7d3eff87d6f706661bf553b18/activerecord/lib/active_record/persistence.rb#L211
[3]:
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html