модуль ActiveRecord::Aggregations::ClassMethods
Active Record реализует агрегацию через макро-подобный класс-метод, называемый composed_of, для представления атрибутов в виде объектов-значений. Он выражает отношения, такие как «Счёт [состоит] из Денег [среди прочего]» или «Человек [состоит] из [адреса]». Каждый вызов макроса добавляет описание того, как объекты-значения создаются из атрибутов объекта сущности (когда сущность инициализируется как новый объект или из поиска существующего объекта) и как их можно преобразовать обратно в атрибуты (когда сущность сохраняется в базе данных).
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#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
Объекты-значения также могут состоять из нескольких атрибутов, как в случае с адресом. Порядок сопоставлений определит порядок параметров.
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"
Создание объектов-значений
Объекты-значения — это неизменяемые и взаимозаменяемые объекты, представляющие данное значение, например, объект Money, представляющий 5 долларов. Два объекта Money, оба представляющие 5 долларов, должны быть равны (через методы, такие как == и <=> из Comparable, если ранжирование имеет смысл). Это отличается от объектов сущностей, где равенство определяется идентичностью. Класс сущности, такой как Клиент, может легко иметь два разных объекта, у которых есть адрес на улице 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: [ %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
Поиск записей по объекту-значению
После указания отношения composed_of для модели, записи могут быть загружены из базы данных, указав экземпляр объекта-значения в хэше условий. Следующий пример находит всех клиентов с address_street равным «Улица Мэй» и address_city равным «Чикаго»:
Customer.where(address: Address.new("May Street", "Chicago"))
Методы публичного экземпляра
# File activerecord/lib/active_record/aggregations.rb, line 222
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 Добавляет методы чтения и записи для управления объектом-значением: composed_of :address добавляет методы address и address=(new_address).
Параметры:
-
:class_name- Указывает имя класса ассоциации. Используйте его только в том случае, если это имя нельзя вывести из идентификатора части. Так,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: %w(reading celsius)
composed_of :balance, class_name: "Money", mapping: %w(balance amount)
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: %w(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) }
© 2004–2020 David Heinemeier Hansson
Licensed under the MIT License.