Spec-Zone.ru › Ruby on Rails 5.0

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

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

class Customer < ActiveRecord::Base
  composed_of :balance, class_name: "Money", mapping: %w(amount currency)
  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 для модели, записи могут быть загружены из базы данных путем указания экземпляра объекта-значения в хеше условий. Следующий пример находит всех клиентов с balance_amount равным 20 и balance_currency равным «USD»:

Customer.where(balance: Money.new(20, "USD"))

Методы публичного экземпляра

composed_of(part_id, options = {}) Показать исходный код
# File activerecord/lib/active_record/aggregations.rb, line 224
def composed_of(part_id, options = {})
  options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)

  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),
                      converter: Proc.new { |balance| balance.to_money }
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–2018 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

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