Spec-Zone.ru › Ruby on Rails 4.2

модуль 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_street = "Vesterbrogade"
customer.address        # => Address.new("Hyancintvej", "Copenhagen")
customer.clear_aggregation_cache
customer.address        # => Address.new("Vesterbrogade", "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.ruby-doc.org/gems/docs/n/netaddr-1.5.0/NetAddr/CIDR.html). Конструктор класса значений называется 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 212
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