Active Record 聚合¶ ↑
Active Record 通过一个类似宏的类方法 composed_of 来实现聚合,用于将属性表示为值对象。它表达了“账户 [由] 金钱 [以及其他东西] 组成”或“人 [由] 地址组成”之类的关系。每次调用宏都会添加一个描述,说明值对象是如何从实体对象的属性创建的(当实体被初始化为新对象或从现有对象查找时),以及当实体保存到数据库时,它又是如何转回属性的。
class Customer < ActiveRecord::Base composed_of :balance, class_name: "Money", mapping: { balance: :amount } composed_of :address, mapping: { address_street: :street, address_city: :city } end
客户类现在有了以下方法来操作值对象:
-
Customer#balance, Customer#balance=(money) -
Customer#address, Customer#address=(address)
这些方法将与下面描述的值对象一起操作。
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
现在可以通过值对象而不是直接访问数据库中的属性。如果你选择将组合命名为与属性名称相同,那么这将是访问该属性的唯一方式。我们的 balance 属性就是这种情况。你与值对象的交互方式与与其他任何属性的交互方式相同。
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
值对象也可以由多个属性组成,例如 Address。映射的顺序将决定参数的顺序。
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"
编写值对象¶ ↑
值对象是表示给定值的不可变且可互换的对象,例如表示 5 美元的 Money 对象。两个都表示 5 美元的 Money 对象应该是相等的(通过 == 方法和 <=> 方法(如果排序有意义,则来自 Comparable)。这与实体对象不同,实体对象的相等性由标识决定。像 Customer 这样的实体类可以轻松拥有两个不同的对象,它们都拥有 Hyancintvej 的地址。实体标识由对象或关系唯一标识符(如主键)决定。普通的 ActiveRecord::Base 类是实体对象。
同样重要的是将值对象视为不可变的。创建后不要允许 Money 对象修改其金额。而是创建一个具有新值的新 Money 对象。Money#exchange_to 方法就是其中的一个例子。它返回一个新的值对象,而不是更改其自身的值。Active Record 不会持久化通过非写入器方法修改的值对象。
Active Record 通过冻结分配给值对象的任何对象来强制执行不可变性要求。尝试之后修改它将导致 RuntimeError。
在 c2.com/cgi/wiki?ValueObject 上阅读更多关于值对象的信息,以及在 c2.com/cgi/wiki?ValueObjectsShouldBeImmutable 上关于不保持值对象不可变性的危险。
自定义构造函数和转换器¶ ↑
默认情况下,值对象通过调用值类的 new 构造函数来初始化,并将每个映射的属性按 :mapping 选项指定的顺序作为参数传递。如果值类不支持此约定,则 composed_of 允许指定自定义构造函数。
当新值被分配给值对象时,默认假设新值是值类的实例。指定自定义转换器允许在必要时自动将新值转换为值类的实例。
例如,NetworkResource 模型具有 network_address 和 cidr_range 属性,这些属性应该使用 NetAddr::CIDR 值类进行聚合(www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR)。值类的构造函数名为 create,它期望一个 CIDR 地址字符串作为参数。可以使用另一个 NetAddr::CIDR 对象、字符串或数组来为值对象分配新值。:constructor 和 :converter 选项可用于满足这些要求。
class NetworkResource < ActiveRecord::Base composed_of :cidr, class_name: 'NetAddr::CIDR', mapping: { network_address: :network, 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
通过值对象查找记录¶ ↑
一旦为模型指定了 composed_of 关系,就可以通过在条件哈希中指定值对象的实例来从数据库加载记录。以下示例查找所有 address_street 等于“May Street”且 address_city 等于“Chicago”的客户。
Customer.where(address: Address.new("May Street", "Chicago"))
实例公共方法
composed_of(part_id, options = {}) 链接
添加用于操作值对象的读取器和写入器方法:composed_of :address 添加 address 和 address=(new_address) 方法。
选项包括:
-
:class_name- 指定关联的类名。仅当无法从 part id 推断出该名称时才使用它。因此composed_of :address默认将链接到 Address 类,但如果实际类名是CompanyAddress,您必须使用此选项指定它。 -
:mapping- 指定实体属性到值对象属性的映射。每个映射都表示为一个键值对,其中键是实体属性的名称,值是值对象中属性的名称。映射定义的顺序决定了将属性传递给值类构造函数的顺序。映射可以写成哈希或对数组。 -
:allow_nil- 指定当所有映射的属性都为nil时,不实例化值对象。将值对象设置为nil会将nil写入所有映射的属性。此选项默认为false。 -
:constructor- 一个符号,指定构造函数方法的名称,或一个用于初始化值对象的 Proc。构造函数将接收所有映射的属性(按照:mapping option中定义的顺序)作为参数,并使用它们来实例化一个:class_name对象。默认值为:new。 -
:converter- 一个符号,指定:class_name类方法的名称,或一个当新值被分配给值对象时调用的 Proc。转换器将接收用于赋值的单个值,并且仅在新值不是:class_name的实例时调用。如果将:allow_nil设置为 true,转换器可以返回nil来跳过赋值。
选项示例
composed_of :temperature, mapping: { reading: :celsius } composed_of :balance, class_name: "Money", mapping: { balance: :amount } composed_of :address, mapping: { address_street: :street, address_city: :city } composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] composed_of :gps_location composed_of :gps_location, allow_nil: true composed_of :ip_address, class_name: 'IPAddr', mapping: { ip: :to_i }, constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) }, converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
来源: 显示 | 在 GitHub 上
# File activerecord/lib/active_record/aggregations.rb, line 225 def composed_of(part_id, options = {}) options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter) unless self < Aggregations include Aggregations end name = part_id.id2name class_name = options[:class_name] || name.camelize mapping = options[:mapping] || [ name, name ] mapping = [ mapping ] unless mapping.first.is_a?(Array) allow_nil = options[:allow_nil] || false constructor = options[:constructor] || :new converter = options[:converter] reader_method(name, class_name, mapping, allow_nil, constructor) writer_method(name, class_name, mapping, allow_nil, converter) reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self) Reflection.add_aggregate_reflection self, part_id, reflection end