Spec-Zone.ru › Ruby on Rails 8.1

модуль ActiveRecord::Aggregations::ClassMethods

Подключенные модули:
ActiveRecord::Aggregations

Агрегации Active Record

Active Record реализует агрегацию с помощью метода класса, похожего на макрос, который называется composed_of и позволяет представлять атрибуты в виде объектов-значений. Он описывает отношения вроде «Account [состоит] из Money [среди прочего]» или «Person [состоит] из [объекта] address». Каждый вызов макроса добавляет описание того, как объекты-значения создаются из атрибутов объекта-сущности (при инициализации сущности как нового объекта или при поиске уже существующего объекта) и как их можно преобразовать обратно в атрибуты (при сохранении сущности в базе данных).

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"

Создание объектов-значений

Объекты-значения — это неизменяемые и взаимозаменяемые объекты, представляющие заданное значение, например объект 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: { 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 = {}) Показать исходный код
# 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

Добавляет методы чтения и записи для работы с объектом-значением: 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: { 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) }

© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API