Making #composed_of more useful
Update: my patch finally got added to edge rails.
Active Record allows you to abstract fields into an aggregate object by using the composed_of declaration. This is handy, but the current implementation can be a real pain.
The first and somewhat trivial issue is the composed_of declaration builds a string and evals it to define the attribute accessors. It’s not a big deal, but it’s dirty. The second issue is that aggregate objects are not easy to manipulate in Rails, especially in forms.
Fixing it
So, I’ve written a plugin that overrides the Active Record implementation of composed_of, which allows you to specify a block to convert incoming parameters to the correct type.
Personally, this has been most helpful when using the Money gem:
class Account < ActiveRecord::Base
composed_of :balance, :class_name => "Money", :mapping => %w(cents cents) do |amount|
amount.to_money
end
end
If #balance= receives anything besides a Money object, it will call the block to try to convert the parameter to a Money object.
>> account = Account.new :balance => 100
>> account.balance
=> #<Money:0x2612770 @cents=10000, @currency="USD">
And now it can transparently be used in forms:
<%= text_field :account, :balance %>
This can even be used for more advanced aggregations:
class User < ActiveRecord::Base
composed_of :address, :class_name => "Address"
:mapping => [%w(street street), %w(city city), %w(state state), %w(zip zip)] do |addr|
Address.new(addr[:street], addr[:city], addr[:state], addr[:zip])
end
end
A user can now be created from a hash:
User.new(:address => {:street => "123 A Street", :city => "Somewhere", :state => "NO", :zip => 12345})
I didn’t quite realize all the implications of this extension until I was writing up the docs for this plugin. Active Record magically does type casting for a limited set of types, namely dates and numbers. But this essentially allows you to have a form of type casting for any attribute. Interesting…
Installation
This has also been submitted as a patch to the Rails trac but hasn’t been accepted yet.
script/plugin install http://source.collectiveidea.com/public/rails/plugins/composed_of_conversion
9 comments
Thanks for this article. I am currently facing a puzzle: is it possible to create ActiveRecord aggregations for objects whose attributes include other objects? If I don’t need to be able to access the attributes of the sub-object, can I just declare the object column as type :binary and be done with it? The Rails docs are mum on this so any hints would be greatly appreciated!
jake:
I’m not sure that I get what you mean, could you give an example?
i had been banging my head on my desk for an hour or so, but you came to my rescue. Hail the liberator! Hail Brandon! I hope your patch gets into rails.
Thanks for this—it was a big help.
What I’d like to be able to do is to pass the value from an instance method that I create into the mapping of “composed_of”.
In both the Rails source and your plugin, the value that gets passed from the model containing the composed_of call into the other model has to be in @attributes.
I made it work by simply changing the read_method method slightly. Instead of reading the attribute, I just call send and pass it the first element of the the mapping.
to
in
Can you think of any negative effects that this might have? Is there a better way to do it?
Thanks!
Dave,
The only issue I can think of is if you want the composed_of object to use the same name as an attribute. For example, if you have a field called balance that you want to be replaced by the composed_of.
Other than that, I think it’s a good idea.
Sweet—I don’t think that will be an issue in my case.
Thanks!
Brandon, I’ve been using your patch for some time and nursing it along with an update lately (update available on dev.rubyonrails.org). I’m tired of this being a patch since it fixes an egregious problem with nil values as well.
Any chance you can shake the trees by posting to the Rails core Google Group? I’ll pile on and back you up, and I know Ian White will as well (he and I worked on an earlier patch that fixed that nil problem).
-Chris
The patch finally got accepted.
Congratulations on the accepted patch, Brandon. I’ve just begun using it in my project together with the Money gem, too.
class Project < ActiveRecord::Base composed_of :budget, :class_name => "Money", :mapping => %w(budget cents), :allow_nil => true do |amount| amount.to_money end endThe backend part works great but I seem to have one problem with displaying the form field in an edit form:
This displays the cents value in the input field (i.e. “24099”) while I obviously would like it to display it in EUR instead (“240.99”). When I then submit the form without changing anything, the “24099” gets converted to a Money object representing 24099 EUR.
Looking at Rails’s form_helper.rb, the reason seems to be that Rails calls @project.budget_before_type_cast to construct the display value for the input field and sure enough, this yields the cents value.
Have you experienced this problem? Or does it work differently for you? Overwriting budget_before_type_cast should do the trick, but I’m curious why nobody else seems to have this problem. I’m on Rails 2.0.1.
Speak your mind: