ri > ActiveRecord::Aggregations::ClassMethods

= ActiveRecord::Aggregations::ClassMethods

------------------------------------------------------------------------
= Includes:
Aggregations (from /home/chedong/.local/share/rdoc)

(from /home/chedong/.local/share/rdoc)
------------------------------------------------------------------------
Active Record implements aggregation through a macro-like class method
called #composed_of for representing attributes as value objects. It
expresses relationships like "Account [is] composed of Money [among
other things]" or "Person [is] composed of [an] address". Each call to
the macro adds a description of how the value objects are created from
the attributes of the entity object (when the entity is initialized
either as a new object or from finding an existing object) and how it
can be turned back into attributes (when the entity is saved to the
database).

  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

The customer class now has the following methods to manipulate the value
objects:
* Customer#balance, Customer#balance=(money)
* Customer#address, Customer#address=(address)

These methods will operate with value objects like the ones described
below:

  class Money
    include Comparable
    attr_reader :amount, :currency
    EXCHANGE_RATES = { "USD_TO_DKK" => 6 }

    def initialize(amount, currency = "USD")
      @amount, @currency = amount, currency
    end

    def exchange_to(other_currency)
      exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor
      Money.new(exchanged_amount, other_currency)
    end

    def ==(other_money)
      amount == other_money.amount && currency == other_money.currency
    end

    def <=>(other_money)
      if currency == other_money.currency
        amount <=> other_money.amount
      else
        amount <=> other_money.exchange_to(currency).amount
      end
    end
  end

  class Address
    attr_reader :street, :city
    def initialize(street, city)
      @street, @city = street, city
    end

    def close_to?(other_address)
      city == other_address.city
    end

    def ==(other_address)
      city == other_address.city && street == other_address.street
    end
  end

Now it's possible to access attributes from the database through the
value objects instead. If you choose to name the composition the same as
the attribute's name, it will be the only way to access that attribute.
That's the case with our balance attribute. You interact with the value
objects just like you would with any other attribute:

  customer.balance = Money.new(20)     # sets the Money value object and the attribute
  customer.balance                     # => Money value object
  customer.balance.exchange_to("DKK")  # => Money.new(120, "DKK")
  customer.balance > Money.new(10)     # => true
  customer.balance == Money.new(20)    # => true
  customer.balance < Money.new(5)      # => false

Value objects can also be composed of multiple attributes, such as the
case of Address. The order of the mappings will determine the order of
the parameters.

  customer.address_street = "Hyancintvej"
  customer.address_city   = "Copenhagen"
  customer.address        # => Address.new("Hyancintvej", "Copenhagen")

  customer.address = Address.new("May Street", "Chicago")
  customer.address_street # => "May Street"
  customer.address_city   # => "Chicago"

== Writing value objects

Value objects are immutable and interchangeable objects that represent a
given value, such as a Money object representing $5. Two Money objects
both representing $5 should be equal (through methods such as == and <=>
from Comparable if ranking makes sense). This is unlike entity objects
where equality is determined by identity. An entity class such as
Customer can easily have two different objects that both have an address
on Hyancintvej. Entity identity is determined by object or relational
unique identifiers (such as primary keys). Normal ActiveRecord::Base
classes are entity objects.

It's also important to treat the value objects as immutable. Don't allow
the Money object to have its amount changed after creation. Create a new
Money object with the new value instead. The Money#exchange_to method is
an example of this. It returns a new value object instead of changing
its own values. Active Record won't persist value objects that have been
changed through means other than the writer method.

The immutable requirement is enforced by Active Record by freezing any
object assigned as a value object. Attempting to change it afterwards
will result in a RuntimeError.

Read more about value objects on http://c2.com/cgi/wiki?ValueObject and
on the dangers of not keeping value objects immutable on
http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable

== Custom constructors and converters

By default value objects are initialized by calling the new constructor
of the value class passing each of the mapped attributes, in the order
specified by the :mapping option, as arguments. If the value class
doesn't support this convention then #composed_of allows a custom
constructor to be specified.

When a new value is assigned to the value object, the default assumption
is that the new value is an instance of the value class. Specifying a
custom converter allows the new value to be automatically converted to
an instance of value class if necessary.

For example, the NetworkResource model has network_address and
cidr_range attributes that should be aggregated using the NetAddr::CIDR
value class (https://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR).
The constructor for the value class is called create and it expects a
CIDR address string as a parameter. New values can be assigned to the
value object using either another NetAddr::CIDR object, a string or an
array. The :constructor and :converter options can be used to meet these
requirements:

  class NetworkResource < ActiveRecord::Base
    composed_of :cidr,
                class_name: 'NetAddr::CIDR',
                mapping: [ %w(network_address network), %w(cidr_range bits) ],
                allow_nil: true,
                constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
                converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
  end

  # This calls the :constructor
  network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)

  # These assignments will both use the :converter
  network_resource.cidr = [ '192.168.2.1', 8 ]
  network_resource.cidr = '192.168.0.1/24'

  # This assignment won't use the :converter as the value is already an instance of the value class
  network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8')

  # Saving and then reloading will use the :constructor on reload
  network_resource.save
  network_resource.reload

== Finding records by a value object

Once a #composed_of relationship is specified for a model, records can
be loaded from the database by specifying an instance of the value
object in the conditions hash. The following example finds all customers
with address_street equal to "May Street" and address_city equal to
"Chicago":

  Customer.where(address: Address.new("May Street", "Chicago"))
------------------------------------------------------------------------
= Instance methods:

  composed_of

ActiveRecord::Aggregations::ClassMethods
ActiveRecord::Aggregations::ClassMethods Includes:
Writing value objects Custom constructors and converters Finding records by a value object
Instance methods:

Generated by phpman v4.9.26-1-g511901d · Markdown · JSON · MCP Author: Che Dong Under GNU General Public License
2026-07-28 20:32 @216.73.217.46
CrawledBy Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com)
Valid XHTML 1.0 Transitional!Valid CSS!

^_top_^