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
- 指定关联的类名。仅当该名称无法从部件 ID 推断出来时才使用它。因此,composed_of :address
默认情况下将链接到 Address 类,但如果真实的类名是CompanyAddress
,则需要使用此选项来指定它。 -
:mapping
- 指定实体属性到值对象属性的映射。每个映射都表示为一个键值对,其中键是实体属性的名称,值是值对象中属性的名称。定义映射的顺序决定了属性发送到值类构造函数的顺序。映射可以写成哈希或成对数组。 -
:allow_nil
- 指定当所有映射属性都为nil
时,不会实例化值对象。将值对象设置为nil
的效果是将nil
写入所有映射的属性。默认值为false
。 -
:constructor
- 一个符号,指定构造函数方法的名称,或一个用于初始化值对象的 Proc。构造函数将按:mapping 选项
中定义的顺序将所有映射的属性作为参数传递,并使用它们来实例化一个: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