Ruby on Rails
UnderstandingAggregation

What is aggregation?

Aggregations let you access attributes on an ActiveRecord object through sort of proxy objects. Taking the examples from the API doc, you can access the fields ‘balance’, ‘address_street’, and ‘address_city’ through the Money and Address objects instead of working directly with the integers and strings.

Take a look at the example on the Aggregation API page.

For example, if you get one this customer object from the database:


class Customer < ActiveRecord::Base end

@customer = Customer.find(20)


Then, accessing the attributes gives you Fixnums and Strings:

p @customer.balance => 400 p @customer.balance.class => Fixnum p @customer.address_street => "123 Big St" p @customer.address_street.class => String p @customer.address_city => "Big City" p @customer.address_city.class => String p @customer.address => undefined method `address'

But, if you use the code from the Aggregation example:

class Customer < ActiveRecord::Base composed_of :balance, :class_name => "Money", :mapping => %w(balance amount) composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ] end

@customer = Customer.find(20)

p @customer.balance => 400
p @customer.balance.class => Money
p @customer.address_street => “123 Big St”
p @customer.address_street.class => String
p @customer.address_city => “Big City”
p @customer.address_city.class => String
p @customer.address => “123 Big St. Big City”
p @customer.address.class => Address

Notice that the balance attribute is now an instance of the Money class and you can’t access the old Fixnum version directly. The address attribute, on the other hand, is a new attribute and you can still access the old attributes.

Basically this allows you to use your own classes for ActiveRecord attributes in addition to the ones defined by the database adaptors.

What goes on behind the scenes.

This call:


composed_of :address, :mapping => [%w(address_street street), %w(address_city city)]

creates code like this in the Customer class:

def address(force_reload = false) @address = Address.new(read_attribute("address_street"), read_attribute("address_city")) end

def address=(part)
[@attributes‘address_street’, @attributes‘address_city’] = [part.street, part.city]
end

Questions

What about objects that need setters, as opposed to new/instantiate?

You would have to create an intermediate object between the object that needs setters and the attributes. That intermediate object would have an initialize method that created your object and then call the individual setters on that object.

Why do aggregations need to be immutable value objects?

You’d have to ask DHH to be sure, but this requirement seems to be a design optimization. You’ll note that the implementation of composed_of keeps two copies of the relevant data around: one in the attributes on the model, and another in the aggregate object. ActiveRecord::Base.save() only utilizes the attributes on the model, and ignores the aggregate object. So, if you were able to modify the object, you would be disappointed when you called save() and it didn’t save your changes. Future versions of Rails (or plugins) could address this issue by redirecting all attribute accessors and setters as well as the save() method to reference the aggregate object instead of the model attributes, but this might lead to performance problems.

Actually it’s not directly an optimization issue; it’s a philosophical decision. The key is understanding the difference between a Value Object and an Entity Object. A Value Object is something which is not assigned by reference (in Ruby anyway). Things like strings and numbers, for instance. An Entity Object is something which is assigned by reference, such as regular Ruby objects such as ActiveRecord models. The core issue is one of identity. A Value Object is identified by its value. If you change something from 2 to 4, has the number 2 changed? No, the number 2 is immutable, we don’t think of changing it to 4, we think of changing whatever had the value of 2 to have a different value. The difference is subtle, but important. Consider strings if they were assigned by reference; if you assigned the same string to two variables and then later changed one of the strings, then the other would be different as well. Entity Objects, on the other hand, have an identity independent of their value. Consider someone’s bank account, it’s still the same account when the balance changes, or they change their address. In general, more complex things should be Entity Objects and simpler things should be Value Objects. This not only makes sense intuitively, but also is more memory efficient for a computer program.

It’s possible to create quite complex aggregations, but the Rails team has clearly drawn a line between Aggregations and Models. If something needs to be an Entity (ie. have an identity independent of its value) than it should be a Model, plain and simple. Aggregations are simply a way of creating data structures more complex than a string or an int or a float, yet are still nothing more than a collection of values.

Rails doesn’t seem to properly handle calls like customer.balance=“400” (by setting balance equal to Money.new(400)). This means that controller code using customer.updateattributes(params[:customer]) doesn’t work as expected. I tried submitting balance as a multiparameter value (balance(1i) and balance(1money)), to no avail. Anyone have any idea if rails is supposed to cast aggregations as it does other types? Thanks!__

I’m also not sure if this is a bug, but I do have a workaround: create a pair of proxy attribute methods that do the conversion. For Money:


composed_of :base_money, :class_name=> "Money", :mapping => [%w(cents cents), %w(currency currency)] def money(<b style="color:black;background-color:#a0ffff">force_reload</b>=false) base_money(<b style="color:black;background-color:#a0ffff">force_reload</b>) end def money=(value) self.base_money=value.to_money end

Another Answer: I believe there is another solution to this problem: _fields_for_

Here is an article to show how to persist composed_of objects, without forcing you to add controller action code.

Rails composed_of &block conversion

See also