модуль 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, если ранжирование имеет смысл). Это отличается от объектов сущностей, где равенство определяется по идентичности. Класс сущности, такой как 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: [ %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 равным «May Street» и address_city равным «Chicago»:
Customer.where(address: Address.new("May Street", "Chicago"))
Общедоступные методы экземпляров
# File activerecord/lib/active_record/aggregations.rb, line 223
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–2019 David Heinemeier Hansson
Licensed under the MIT License.